Merge branch '369-duplicate-evaluation-document' into testing202509

This commit is contained in:
2025-09-02 17:54:23 +02:00
32 changed files with 1414 additions and 754 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

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Duplication of a document to another accompanying period work evaluation
time: 2025-04-03T10:03:11.796736107+02:00
custom:
Issue: "369"
SchemaChange: No schema change

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Fusion of two accompanying period works
time: 2025-04-03T10:08:57.25079018+02:00
custom:
Issue: "359"
SchemaChange: No schema change

View File

@@ -4,6 +4,7 @@ import { StoredObject, StoredObjectVersion } from "../../types";
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
import { computed, reactive } from "vue";
import { useToast } from "vue-toast-notification";
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
interface DropFileConfig {
allowRemove: boolean;
@@ -75,10 +76,10 @@ function closeModal(): void {
@click="openModal"
class="btn btn-create"
>
Ajouter un document
{{ trans(DOCUMENT_ADD) }}
</button>
<button v-else @click="openModal" class="btn btn-edit">
Remplacer le document
<button v-else @click="openModal" class="dropdown-item">
{{ trans(DOCUMENT_REPLACE) }}
</button>
<modal
v-if="state.showModal"

View File

@@ -23,6 +23,8 @@ See the document: Voir le document
document:
Any title: Aucun titre
replace: Remplacer
Add: Ajouter un document
generic_doc:
filter:

View File

@@ -280,11 +280,17 @@
</div>
{% endblock %}
{% block pick_linked_entities_widget %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
<div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
></div>
{% block pick_linked_entities_widget %}
<input type="hidden" {{ block('widget_attributes') }}
{% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %}
data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div
data-input-uniqid="{{ form.vars['uniqid'] }}"
data-module="pick-linked-entities"
data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
></div>
{% endblock %}
{% block pick_postal_code_widget %}

View File

@@ -18,6 +18,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatableMessage;
@@ -32,7 +33,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
* @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
*/
#[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request): Response
{
$accompanyingPeriod = $acpw->getAccompanyingPeriod();
@@ -79,7 +80,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
*/
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request): Response
{
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
@@ -98,6 +99,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
$session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
}
return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
@@ -24,15 +25,16 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
{
public function __construct(
private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
private readonly Security $security,
private readonly SerializerInterface $serializer,
private readonly EntityManagerInterface $entityManager,
private readonly ChillUrlGeneratorInterface $urlGenerator,
private AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
private Security $security,
private SerializerInterface $serializer,
private EntityManagerInterface $entityManager,
private ChillUrlGeneratorInterface $urlGenerator,
) {}
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
@@ -56,6 +58,32 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
);
}
/**
* @ParamConverter("document", options={"id": "document_id"})
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
*/
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate', methods: ['POST'])]
public function duplicateToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
{
$work = $evaluation->getAccompanyingPeriodWork();
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
}
$duplicatedDocument = $this->duplicator->duplicateToEvaluation($document, $evaluation);
$this->entityManager->persist($duplicatedDocument);
$this->entityManager->persist($duplicatedDocument->getStoredObject());
$this->entityManager->persist($evaluation);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
#[Route('/{_locale}/person/accompanying-course-work-evaluation-document/{id}/duplicate', name: 'chill_person_accompanying_period_work_evaluation_document_duplicate', methods: ['POST'])]
public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): Response
{

View File

@@ -0,0 +1,58 @@
<?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\Controller;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
readonly class AccompanyingPeriodWorkEvaluationDocumentMoveController
{
public function __construct(
private Security $security,
private SerializerInterface $serializer,
private EntityManagerInterface $entityManager,
) {}
/**
* @ParamConverter("document", options={"id": "document_id"})
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
*/
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/move', methods: ['POST'])]
public function moveToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
{
$work = $evaluation->getAccompanyingPeriodWork();
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
}
$document->setAccompanyingPeriodWorkEvaluation($evaluation);
$this->entityManager->persist($document);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
}

View File

@@ -98,16 +98,6 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument
{
// if an evaluation is already associated, we cannot change the association (removing the association,
// by setting a null value, is allowed.
if (
$this->accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
&& $accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
) {
if ($this->accompanyingPeriodWorkEvaluation !== $accompanyingPeriodWorkEvaluation) {
throw new \RuntimeException('It is not allowed to change the evaluation for a document');
}
}
$this->accompanyingPeriodWorkEvaluation = $accompanyingPeriodWorkEvaluation;
return $this;

View File

@@ -24,7 +24,7 @@ class FindAccompanyingPeriodWorkType extends AbstractType
{
$builder
->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
'label' => 'Social action',
'label' => 'Accompanying period work',
'multiple' => false,
'accompanyingPeriod' => $options['accompanyingPeriod'],
])

View File

@@ -16,18 +16,26 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
{
public function __construct(private readonly NormalizerInterface $normalizer) {}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['acpw'];
$view->vars['uniqid'] = uniqid('pick_acpw_dyn');
$view->vars['suggested'] = [];
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
$view->vars['submit_on_adding_new_entity'] = false;
$view->vars['pick-entities-type'] = 'acpw';
$view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId();
foreach ($options['suggested'] as $suggestion) {
$view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']);
}
}
public function configureOptions(OptionsResolver $resolver)
@@ -38,6 +46,7 @@ class PickLinkedAccompanyingPeriodWorkType extends AbstractType
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false)
->setDefault('suggested', [])
->setDefault('as_id', false)
->setAllowedTypes('as_id', ['bool'])
->setDefault('submit_on_adding_new_entity', false)

View File

@@ -41,7 +41,8 @@ document.addEventListener("DOMContentLoaded", () => {
methods: {
pickWork: function (payload: { work: AccompanyingPeriodWork }) {
console.log("payload", payload);
input.value = payload.work.id.toString();
input.value = payload.work.id?.toString() ?? "";
},
},
});

View File

@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
}
export interface AccompanyingPeriodWork {
id: number;
id?: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;

View File

@@ -972,7 +972,7 @@ div#workEditor {
font-size: 85%;
}
i.fa {
& > i.fa {
padding: 0.25rem;
color: $white;

View File

@@ -0,0 +1,31 @@
<template>
<div class="row mb-3">
<label class="col-sm-4 col-form-label visually-hidden">{{
trans(EVALUATION_PUBLIC_COMMENT)
}}</label>
<div class="col-sm-12">
<ckeditor
:editor="ClassicEditor"
:config="classicEditorConfig"
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
:value="comment"
@input="$emit('update:comment', $event)"
tag-name="textarea"
></ckeditor>
</div>
</div>
</template>
<script setup>
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
import { ClassicEditor } from "ckeditor5";
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
import {
EVALUATION_PUBLIC_COMMENT,
EVALUATION_COMMENT_PLACEHOLDER,
trans,
} from "translator";
defineProps(["comment"]);
defineEmits(["update:comment"]);
</script>

View File

@@ -0,0 +1,71 @@
<template>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_STARTDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="startDate"
@input="$emit('update:startDate', $event.target.value)"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_ENDDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="endDate"
@input="$emit('update:endDate', $event.target.value)"
/>
</div>
</div>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_MAXDATE) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
:value="maxDate"
@input="$emit('update:maxDate', $event.target.value)"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_WARNING_INTERVAL) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="number"
:value="warningInterval"
@input="$emit('update:warningInterval', $event.target.value)"
/>
</div>
</div>
</template>
<script setup>
import {
EVALUATION_STARTDATE,
EVALUATION_ENDDATE,
EVALUATION_MAXDATE,
EVALUATION_WARNING_INTERVAL,
trans,
} from "translator";
defineProps(["startDate", "endDate", "maxDate", "warningInterval"]);
defineEmits([
"update:startDate",
"update:endDate",
"update:maxDate",
"update:warningInterval",
]);
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="row mb-3">
<h6>{{ trans(EVALUATION_DOCUMENT_ADD) }} :</h6>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id"
:templates="templates"
:preventDefaultMoveToGenerate="true"
@go-to-generate-document="$emit('submitBeforeGenerate', $event)"
>
<template v-slot:title>
<label class="col-form-label">{{
trans(EVALUATION_GENERATE_A_DOCUMENT)
}}</label>
</template>
</pick-template>
<div>
<label class="col-form-label">{{
trans(EVALUATION_DOCUMENT_UPLOAD)
}}</label>
<ul class="record_actions document-upload">
<li>
<drop-file-modal
:allow-remove="false"
@add-document="$emit('addDocument', $event)"
></drop-file-modal>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import {
EVALUATION_DOCUMENT_ADD,
EVALUATION_DOCUMENT_UPLOAD,
EVALUATION_GENERATE_A_DOCUMENT,
trans,
} from "translator";
defineProps(["evaluation", "templates"]);
defineEmits(["addDocument", "submitBeforeGenerate"]);
</script>
<style scoped>
ul.document-upload {
justify-content: flex-start;
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<div class="row mb-3">
<h5>{{ trans(EVALUATION_DOCUMENTS) }} :</h5>
<div class="flex-table">
<div
class="item-bloc"
v-for="(d, i) in documents"
:key="d.id"
:class="[
parseInt(docAnchorId) === d.id ? 'bg-blink' : 'nothing',
]"
>
<div :id="'document_' + d.id" class="item-row">
<div class="input-group input-group-lg mb-3 row">
<label class="col-sm-3 col-form-label"
>Titre du document:</label
>
<div class="col-sm-9">
<input
class="form-control document-title"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="$emit('inputDocumentTitle', $event)"
/>
</div>
</div>
</div>
<div class="item-row">
<div class="item-col item-meta">
<p v-if="d.createdBy" class="createdBy">
Créé par {{ d.createdBy.text }}<br />
Le
{{
$d(ISOToDatetime(d.createdAt.datetime), "long")
}}
</p>
</div>
</div>
<div class="item-row">
<div class="item-col">
<ul class="record_actions">
<li
v-if="
d.workflows_availables.length > 0 ||
d.workflows.length > 0
"
>
<list-workflow-modal
:workflows="d.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
:relatedEntityId="d.id"
:workflowsAvailables="
d.workflows_availables
"
:preventDefaultMoveToGenerate="true"
:goToGenerateWorkflowPayload="{ doc: d }"
@go-to-generate-workflow="
$emit('goToGenerateWorkflow', $event)
"
></list-workflow-modal>
</li>
<li>
<button
v-if="AmIRefferer"
class="btn btn-notify"
@click="
$emit(
'goToGenerateNotification',
d,
false,
)
"
></button>
<template v-else>
<button
id="btnGroupNotifyButtons"
type="button"
class="btn btn-notify dropdown-toggle"
:title="
trans(EVALUATION_NOTIFICATION_SEND)
"
data-bs-toggle="dropdown"
aria-expanded="false"
>
&nbsp;
</button>
<ul
class="dropdown-menu"
aria-labelledby="btnGroupNotifyButtons"
>
<li>
<a
class="dropdown-item"
@click="
$emit(
'goToGenerateNotification',
d,
true,
)
"
>
{{
trans(
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
)
}}
</a>
</li>
<li>
<a
class="dropdown-item"
@click="
$emit(
'goToGenerateNotification',
d,
false,
)
"
>
{{
trans(
EVALUATION_NOTIFICATION_NOTIFY_ANY,
)
}}
</a>
</li>
</ul>
</template>
</li>
<li>
<document-action-buttons-group
:stored-object="d.storedObject"
:filename="d.title"
:can-edit="true"
:execute-before-leave="
submitBeforeLeaveToEditor
"
:davLink="
d.storedObject._links?.dav_link.href
"
:davLinkExpiration="
d.storedObject._links?.dav_link
.expiration
"
@on-stored-object-status-change="
$emit('statusDocumentChanged', $event)
"
></document-action-buttons-group>
</li>
<li v-if="Number.isInteger(d.id)">
<div class="duplicate-dropdown">
<button
class="btn btn-edit dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ trans(EVALUATION_DOCUMENT_EDIT) }}
</button>
<ul class="dropdown-menu">
<!--delete-->
<li v-if="d.workflows.length === 0">
<a
class="dropdown-item"
@click="
$emit('removeDocument', d)
"
>
<i
class="fa fa-trash-o"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DELETE,
)
}}
</a>
</li>
<!--replace document-->
<li
v-if="
d.storedObject._permissions
.canEdit
"
>
<drop-file-modal
:existing-doc="d.storedObject"
:allow-remove="false"
@add-document="
(arg) =>
$emit(
'replaceDocument',
d,
arg.stored_object,
arg.stored_object_version,
)
"
></drop-file-modal>
</li>
<!--duplicate document-->
<li>
<a
class="dropdown-item"
@click="
$emit(
'duplicateDocument',
d,
)
"
>
<i
class="fa fa-copy"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DUPLICATE_HERE,
)
}}
</a>
</li>
<li>
<a
class="dropdown-item"
@click="
prepareDocumentDuplicationToWork(
d,
)
"
>
<i
class="fa fa-copy"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
)
}}</a
>
</li>
<!--move document-->
<li
v-if="
d.storedObject._permissions
.canEdit
"
>
<a
class="dropdown-item"
@click="
prepareDocumentMoveToWork(d)
"
>
<i
class="fa fa-arrows"
aria-hidden="true"
></i>
{{
trans(
EVALUATION_DOCUMENT_MOVE,
)
}}</a
>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<AccompanyingPeriodWorkSelectorModal
v-if="showAccompanyingPeriodSelector"
v-model:selectedAcpw="selectedAcpw"
:accompanying-period-id="accompanyingPeriodId"
:is-evaluation-selector="true"
:ignore-accompanying-period-work-ids="[]"
@close-modal="showAccompanyingPeriodSelector = false"
@update:selectedEvaluation="selectedEvaluation = $event"
/>
</template>
<script setup>
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import {
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
EVALUATION_NOTIFICATION_NOTIFY_ANY,
EVALUATION_NOTIFICATION_SEND,
EVALUATION_DOCUMENTS,
EVALUATION_DOCUMENT_MOVE,
EVALUATION_DOCUMENT_DELETE,
EVALUATION_DOCUMENT_EDIT,
EVALUATION_DOCUMENT_DUPLICATE_HERE,
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
trans,
} from "translator";
import { ref, watch } from "vue";
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
defineProps([
"documents",
"docAnchorId",
"accompanyingPeriodId",
"accompanyingPeriodWorkId",
]);
const emit = defineEmits([
"inputDocumentTitle",
"removeDocument",
"duplicateDocument",
"statusDocumentChanged",
"goToGenerateWorkflow",
"goToGenerateNotification",
"duplicateDocumentToWork",
]);
const showAccompanyingPeriodSelector = ref(false);
const selectedEvaluation = ref(null);
const selectedDocumentToDuplicate = ref(null);
const selectedDocumentToMove = ref(null);
const prepareDocumentDuplicationToWork = (d) => {
selectedDocumentToDuplicate.value = d;
/** ensure selectedDocumentToMove is null */
selectedDocumentToMove.value = null;
showAccompanyingPeriodSelector.value = true;
};
const prepareDocumentMoveToWork = (d) => {
selectedDocumentToMove.value = d;
/** ensure selectedDocumentToDuplicate is null */
selectedDocumentToDuplicate.value = null;
showAccompanyingPeriodSelector.value = true;
};
watch(selectedEvaluation, (val) => {
if (selectedDocumentToDuplicate.value) {
emit("duplicateDocumentToEvaluation", {
evaluation: val,
document: selectedDocumentToDuplicate.value,
});
} else {
emit("moveDocumentToEvaluation", {
evaluationDest: val,
document: selectedDocumentToMove.value,
});
}
});
</script>

View File

@@ -1,394 +1,74 @@
<template>
<div>
<!--h2>
{{ $t('evaluation_title') }}
</h2-->
<div class="m-md-3">
<!--div class="row mb-3">
<label class="col-sm-4 col-form-label">{{ $t('evaluation_status') }}</label>
<div class="col-sm-8">
<select class="form-select form-select-sm" v-model="status">
<option disabled value="">{{ $t('evaluation_choose_a_status') }}</option>
<option v-for="s in listAllStatus" :value="s.id">
{{ s.id }}
</option>
</select>
</div>
</div-->
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ $t("evaluation_startdate") }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
v-model="startDate"
<DateInputs
:startDate="startDate"
:endDate="endDate"
:maxDate="maxDate"
:warningInterval="warningInterval"
@update:startDate="updateStartDate"
@update:endDate="updateEndDate"
@update:maxDate="updateMaxDate"
@update:warningInterval="updateWarningInterval"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ $t("evaluation_enddate") }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
v-model="endDate"
<TimeSpentInput
:timeSpent="timeSpent"
:timeSpentChoices="timeSpentChoices"
@update:timeSpent="updateTimeSpent"
/>
</div>
</div>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ $t("evaluation_maxdate") }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="date"
v-model="maxDate"
<CommentInput :comment="comment" @update:comment="updateComment" />
<DocumentsList
v-if="evaluation.documents.length > 0"
:documents="evaluation.documents"
:docAnchorId="docAnchorId"
:accompanyingPeriodId="store.state.work.accompanyingPeriod.id"
:accompanying-period-work-id="store.state.work.id"
@inputDocumentTitle="onInputDocumentTitle"
@removeDocument="removeDocument"
@duplicateDocument="duplicateDocument"
@duplicate-document-to-evaluation="
duplicateDocumentToEvaluation
"
@move-document-to-evaluation="moveDocumentToEvaluation"
@statusDocumentChanged="onStatusDocumentChanged"
@goToGenerateWorkflow="goToGenerateWorkflowEvaluationDocument"
@goToGenerateNotification="goToGenerateDocumentNotification"
/>
</div>
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ $t("evaluation_warning_interval") }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<input
class="form-control form-control-sm"
type="number"
v-model.number="warningInterval"
/>
</div>
</div>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ $t("evaluation_time_spent") }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<select
class="form-control form-control-sm"
type="time"
v-model="timeSpent"
>
<option disabled value="">
{{ $t("select_time_spent") }}
</option>
<option
v-for="(time, i) in timeSpentChoices"
:value="time.value"
:key="i"
>
{{ time.text }}
</option>
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-4 col-form-label visually-hidden">{{
$t("evaluation_public_comment")
}}</label>
<div class="col-sm-12">
<comment-editor v-model:value="comment"></comment-editor>
</div>
</div>
<div v-if="evaluation.documents.length > 0" class="row mb-3">
<h5>{{ $t("Documents") }} :</h5>
<div class="flex-table">
<div
class="item-bloc"
v-for="(d, i) in evaluation.documents"
:key="d.id"
:class="[
parseInt(this.docAnchorId) === d.id
? 'bg-blink'
: 'nothing',
]"
>
<div :id="`document_${d.id}`" class="item-row">
<div class="input-group input-group-lg mb-3 row">
<label class="col-sm-3 col-form-label"
>Titre du document:</label
>
<div class="col-sm-9">
<input
class="form-control document-title"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="onInputDocumentTitle"
/>
</div>
</div>
</div>
<div class="item-row">
<div class="item-col item-meta">
<p v-if="d.createdBy" class="createdBy">
Créé par {{ d.createdBy.text }}<br />
Le
{{
$d(
ISOToDatetime(d.createdAt.datetime),
"long",
)
}}
</p>
</div>
</div>
<div class="item-row">
<div class="item-col">
<ul class="record_actions">
<li
v-if="
d.workflows_availables.length > 0 ||
d.workflows.length > 0
"
>
<list-workflow-modal
:workflows="d.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
:relatedEntityId="d.id"
:workflowsAvailables="
d.workflows_availables
"
:preventDefaultMoveToGenerate="true"
:goToGenerateWorkflowPayload="{
doc: d,
}"
@go-to-generate-workflow="
goToGenerateWorkflowEvaluationDocument
"
></list-workflow-modal>
</li>
<li>
<button
v-if="AmIRefferer"
class="btn btn-notify"
@click="
goToGenerateDocumentNotification(
d,
false,
)
"
></button>
<template v-else>
<button
id="btnGroupNotifyButtons"
type="button"
class="btn btn-notify dropdown-toggle"
:title="$t('notification_send')"
data-bs-toggle="dropdown"
aria-expanded="false"
>
&nbsp;
</button>
<ul
class="dropdown-menu"
aria-labelledby="btnGroupNotifyButtons"
>
<li>
<a
class="dropdown-item"
@click="
goToGenerateDocumentNotification(
d,
true,
)
"
>{{
$t(
"notification_notify_referrer",
)
}}</a
>
</li>
<li>
<a
class="dropdown-item"
@click="
goToGenerateDocumentNotification(
d,
false,
)
"
>{{
$t(
"notification_notify_any",
)
}}</a
>
</li>
</ul>
</template>
</li>
<li>
<document-action-buttons-group
:stored-object="d.storedObject"
:filename="d.title"
:can-edit="true"
:execute-before-leave="
submitBeforeLeaveToEditor
"
:davLink="
d.storedObject._links?.dav_link
.href
"
:davLinkExpiration="
d.storedObject._links?.dav_link
.expiration
"
@on-stored-object-status-change="
onStatusDocumentChanged
"
></document-action-buttons-group>
</li>
<li
v-if="
d.storedObject._permissions.canEdit
"
>
<drop-file-modal
:existing-doc="d.storedObject"
:allow-remove="false"
@add-document="
(arg) =>
replaceDocument(
d,
arg.stored_object,
arg.stored_object_version,
)
"
></drop-file-modal>
</li>
<li v-if="d.workflows.length === 0">
<a
class="btn btn-delete"
@click="removeDocument(d)"
>
</a>
</li>
<li v-if="Number.isInteger(d.id)">
<button
type="button"
@click="duplicateDocument(d)"
class="btn btn-duplicate"
title="Dupliquer"
></button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<h6>{{ $t("document_add") }} :</h6>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id"
<DocumentActions
:evaluation="evaluation"
:templates="getTemplatesAvailables"
:preventDefaultMoveToGenerate="true"
@go-to-generate-document="submitBeforeGenerate"
>
<template v-slot:title>
<label class="col-form-label">{{
$t("evaluation_generate_a_document")
}}</label>
</template>
</pick-template>
<div>
<label class="col-form-label">{{
$t("document_upload")
}}</label>
<ul class="record_actions document-upload">
<li>
<drop-file-modal
:allow-remove="false"
@add-document="addDocument"
></drop-file-modal>
</li>
</ul>
</div>
</div>
@addDocument="addDocument"
@submitBeforeGenerate="submitBeforeGenerate"
/>
</div>
</div>
</template>
<script>
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
import { mapState } from "vuex";
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
import { buildLink } from "ChillDocGeneratorAssets/lib/document-generator";
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
import { buildLinkCreate } from "ChillMainAssets/lib/entity-workflow/api";
import { buildLinkCreate as buildLinkCreateNotification } from "ChillMainAssets/lib/entity-notification/api";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
<script setup>
import { computed } from "vue";
import { useStore } from "vuex";
import DateInputs from "./DateInputs.vue";
import TimeSpentInput from "./TimeSpentInput.vue";
import CommentInput from "./CommentInput.vue";
import DocumentsList from "./DocumentsList.vue";
import DocumentActions from "./DocumentActions.vue";
import {
trans,
EVALUATION_DOCUMENT_DUPLICATE_SUCCESS,
EVALUATION_DOCUMENT_MOVE_SUCCESS,
} from "translator";
import { useToast } from "vue-toast-notification";
const i18n = {
messages: {
fr: {
evaluation_title: "Ecrire une évaluation",
evaluation_status: "Statut",
evaluation_choose_a_status: "Choisir un statut",
evaluation_startdate: "Date d'ouverture",
evaluation_enddate: "Date de fin",
evaluation_maxdate: "Date d'échéance",
evaluation_warning_interval: "Rappel (jours)",
evaluation_public_comment: "Note publique",
evaluation_comment_placeholder: "Commencez à écrire ...",
evaluation_generate_a_document: "Générer un document",
evaluation_choose_a_template: "Choisir un modèle",
evaluation_add_a_document: "Ajouter un document",
evaluation_add: "Ajouter une évaluation",
evaluation_time_spent: "Temps de rédaction",
select_time_spent: "Indiquez le temps de rédaction",
Documents: "Documents",
document_add: "Générer ou téléverser un document",
document_upload: "Téléverser un document",
document_title: "Titre du document",
template_title: "Nom du template",
browse: "Ajouter un document",
replace: "Remplacer",
download: "Télécharger le fichier existant",
notification_notify_referrer: "Notifier le référent",
notification_notify_any: "Notifier d'autres utilisateurs",
notification_send: "Envoyer une notification",
},
},
};
const props = defineProps(["evaluation", "docAnchorId"]);
const store = useStore();
export default {
name: "FormEvaluation",
props: ["evaluation", "docAnchorId"],
components: {
CommentEditor,
DropFileModal,
PickTemplate,
ListWorkflowModal,
DocumentActionButtonsGroup,
},
i18n,
data() {
return {
template: null,
asyncUploadOptions: {
maxFiles: 1,
maxPostSize: 15000000,
required: false,
},
timeSpentChoices: [
const $toast = useToast();
const timeSpentChoices = [
{ text: "1 minute", value: 60 },
{ text: "2 minutes", value: 120 },
{ text: "3 minutes", value: 180 },
@@ -417,207 +97,137 @@ export default {
{ text: "7 hours", value: 25200 },
{ text: "7 hours 30 minutes", value: 27000 },
{ text: "8 hours", value: 28800 },
],
};
},
computed: {
...mapState(["isPosting", "work", "me"]),
AmIRefferer() {
return !(
this.$store.state.work.accompanyingPeriod.user &&
this.$store.state.me &&
this.$store.state.work.accompanyingPeriod.user.id !==
this.$store.state.me.id
);
},
getTemplatesAvailables() {
return this.$store.getters.getTemplatesAvailablesForEvaluation(
this.evaluation.evaluation,
);
},
canGenerate() {
return !this.$store.state.isPosting && this.template !== null;
},
startDate: {
];
const startDate = computed({
get() {
console.log("evaluation", this.evaluation);
return this.evaluation.startDate;
return props.evaluation.startDate;
},
set(v) {
this.$store.commit("setEvaluationStartDate", {
key: this.evaluation.key,
store.commit("setEvaluationStartDate", {
key: props.evaluation.key,
date: v,
});
},
},
endDate: {
});
const endDate = computed({
get() {
return this.evaluation.endDate;
return props.evaluation.endDate;
},
set(v) {
this.$store.commit("setEvaluationEndDate", {
key: this.evaluation.key,
store.commit("setEvaluationEndDate", {
key: props.evaluation.key,
date: v,
});
},
},
maxDate: {
});
const maxDate = computed({
get() {
return this.evaluation.maxDate;
return props.evaluation.maxDate;
},
set(v) {
this.$store.commit("setEvaluationMaxDate", {
key: this.evaluation.key,
store.commit("setEvaluationMaxDate", {
key: props.evaluation.key,
date: v,
});
},
},
warningInterval: {
});
const warningInterval = computed({
get() {
return this.evaluation.warningInterval;
return props.evaluation.warningInterval;
},
set(v) {
this.$store.commit("setEvaluationWarningInterval", {
key: this.evaluation.key,
store.commit("setEvaluationWarningInterval", {
key: props.evaluation.key,
days: v,
});
},
},
timeSpent: {
});
const timeSpent = computed({
get() {
return this.evaluation.timeSpent;
return props.evaluation.timeSpent;
},
set(v) {
this.$store.commit("setEvaluationTimeSpent", {
key: this.evaluation.key,
store.commit("setEvaluationTimeSpent", {
key: props.evaluation.key,
time: v,
});
},
},
comment: {
});
const comment = computed({
get() {
return this.evaluation.comment;
return props.evaluation.comment;
},
set(v) {
this.$store.commit("setEvaluationComment", {
key: this.evaluation.key,
store.commit("setEvaluationComment", {
key: props.evaluation.key,
comment: v,
});
},
},
},
methods: {
ISOToDatetime,
listAllStatus() {
console.log("load all status");
let url = `/api/`;
fetch(url).then((response) => {
if (response.ok) {
return response.json();
});
const getTemplatesAvailables = computed(() => {
return store.getters.getTemplatesAvailablesForEvaluation(
props.evaluation.evaluation,
);
});
// const getAccompanyingPeriod = computed(() => store.work)
function updateStartDate(value) {
startDate.value = value;
}
throw { m: "yeeah", s: response.status, b: response.body };
});
},
buildEditLink(document) {
return (
`/chill/wopi/edit/${document.storedObject.uuid}?returnPath=` +
encodeURIComponent(
window.location.pathname +
window.location.search +
window.location.hash,
)
);
},
submitBeforeLeaveToEditor() {
console.log("submit beore edit 2");
// empty callback
const callback = () => null;
return this.$store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
},
submitBeforeEdit(storedObject) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === this.evaluation.key,
);
let document = evaluation.documents.find(
(d) => d.storedObject.id === storedObject.id,
);
//console.log('=> document', document);
window.location.assign(this.buildEditLink(document));
};
return this.$store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
},
submitBeforeGenerate({ template }) {
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === this.evaluation.key,
).id;
window.location.assign(
buildLink(
template,
evaluationId,
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
),
);
};
function updateEndDate(value) {
endDate.value = value;
}
return this.$store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
},
onInputDocumentTitle(event) {
function updateMaxDate(value) {
maxDate.value = value;
}
function updateWarningInterval(value) {
warningInterval.value = value;
}
function updateTimeSpent(value) {
timeSpent.value = value;
}
function updateComment(value) {
comment.value = value;
}
function onInputDocumentTitle(event) {
const id = Number(event.target.id);
const key = Number(event.target.dataset.key) + 1;
const title = event.target.value;
this.$store.commit("updateDocumentTitle", {
store.commit("updateDocumentTitle", {
id: id,
key: key,
evaluationKey: this.evaluation.key,
evaluationKey: props.evaluation.key,
title: title,
});
},
addDocument({ stored_object, stored_object_version, file_name }) {
}
function addDocument({ stored_object, stored_object_version }) {
let document = {
type: "accompanying_period_work_evaluation_document",
storedObject: stored_object,
title: file_name,
title: "Nouveau document",
};
this.$store.commit("addDocument", {
key: this.evaluation.key,
store.commit("addDocument", {
key: props.evaluation.key,
document,
stored_object_version,
});
},
/**
* Replaces a document in the store with a new document.
*
* @param {Object} oldDocument - The document to be replaced.
* @param {StoredObject} storedObject - The stored object of the new document.
* @param {StoredObjectVersion} storedObjectVersion - The new version of the document
* @return {void}
*/
replaceDocument(oldDocument, storedObject, storedObjectVersion) {
let document = {
type: "accompanying_period_work_evaluation_document",
storedObject: storedObject,
title: oldDocument.title,
};
this.$store.commit("replaceDocument", {
key: this.evaluation.key,
document,
oldDocument: oldDocument,
stored_object_version: storedObjectVersion,
});
},
removeDocument(document) {
}
function removeDocument(document) {
if (
window.confirm(
'Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre "' +
@@ -625,29 +235,65 @@ export default {
'" ?',
)
) {
this.$store.commit("removeDocument", {
key: this.evaluation.key,
store.commit("removeDocument", {
key: props.evaluation.key,
document: document,
});
}
},
duplicateDocument(document) {
this.$store.dispatch("duplicateDocument", {
evaluation_key: this.evaluation.key,
}
function duplicateDocument(document) {
store.dispatch("duplicateDocument", {
evaluation_key: props.evaluation.key,
document: document,
});
},
onStatusDocumentChanged(newStatus) {
console.log("onStatusDocumentChanged", newStatus);
this.$store.commit("statusDocumentChanged", {
key: this.evaluation.key,
}
function duplicateDocumentToEvaluation({ evaluation, document }) {
store
.dispatch("duplicateDocumentToEvaluation", {
evaluation: evaluation,
document: document,
})
.then(() => {
$toast.open({
message: trans(EVALUATION_DOCUMENT_DUPLICATE_SUCCESS),
});
})
.catch((e) => {
console.log(e);
});
}
function moveDocumentToEvaluation({ evaluationDest, document }) {
console.log("dest eval in formEvaluation", evaluationDest);
store
.dispatch("moveDocumentToEvaluation", {
evaluationInitial: props.evaluation,
evaluationDest: evaluationDest,
document: document,
})
.then(() => {
$toast.open({
message: trans(EVALUATION_DOCUMENT_MOVE_SUCCESS),
});
})
.catch((e) => {
console.log(e);
});
}
function onStatusDocumentChanged(newStatus) {
store.commit("statusDocumentChanged", {
key: props.evaluation.key,
newStatus: newStatus,
});
},
goToGenerateWorkflowEvaluationDocument({ workflowName, payload }) {
}
function goToGenerateWorkflowEvaluationDocument({ workflowName, payload }) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === this.evaluation.key,
(e) => e.key === props.evaluation.key,
);
let updatedDocument = evaluation.documents.find(
(d) => d.key === payload.doc.key,
@@ -661,15 +307,16 @@ export default {
);
};
return this.$store.dispatch("submit", callback).catch((e) => {
store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
},
goToGenerateDocumentNotification(document, tos) {
}
function goToGenerateDocumentNotification(document, tos) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(
(e) => e.key === this.evaluation.key,
(e) => e.key === props.evaluation.key,
);
let updatedDocument = evaluation.documents.find(
(d) => d.key === document.key,
@@ -679,7 +326,7 @@ export default {
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
updatedDocument.id,
tos === true
? this.$store.state.work.accompanyingPeriod.user.id
? store.state.work.accompanyingPeriod.user.id
: null,
window.location.pathname +
window.location.search +
@@ -687,13 +334,11 @@ export default {
),
);
};
return this.$store.dispatch("submit", callback).catch((e) => {
store.dispatch("submit", callback).catch((e) => {
console.log(e);
throw e;
});
},
},
};
}
</script>
<style lang="scss" scoped>
@@ -701,9 +346,6 @@ input.document-title {
font-weight: bold;
font-size: 1rem;
}
ul.document-upload {
justify-content: flex-start;
}
.bg-blink {
color: #050000;

View File

@@ -0,0 +1,32 @@
<template>
<div class="row mb-3">
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
{{ trans(EVALUATION_TIME_SPENT) }}
</label>
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
<select
class="form-control form-control-sm"
:value="timeSpent"
@input="$emit('update:timeSpent', $event.target.value)"
>
<option disabled value="">
{{ trans(EVALUATION_TIME_SPENT) }}
</option>
<option
v-for="time in timeSpentChoices"
:value="time.value"
:key="time.value"
>
{{ time.text }}
</option>
</select>
</div>
</div>
</template>
<script setup>
import { EVALUATION_TIME_SPENT, trans } from "translator";
defineProps(["timeSpent", "timeSpentChoices"]);
defineEmits(["update:timeSpent"]);
</script>

View File

@@ -11,12 +11,13 @@ import { findSocialActionsBySocialIssue } from "ChillPersonAssets/vuejs/_api/Soc
import { create } from "ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js";
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
import { duplicate } from "../_api/accompanyingCourseWorkEvaluationDocument";
import {
duplicate,
duplicateDocumentToEvaluation,
moveDocumentToEvaluation,
} from "../_api/accompanyingCourseWorkEvaluationDocument";
const debug = process.env.NODE_ENV !== "production";
const evalFQDN = encodeURIComponent(
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
);
const store = createStore({
strict: debug,
@@ -298,15 +299,47 @@ const store = createStore({
);
},
addDuplicatedDocument(state, { document, evaluation_key }) {
console.log("add duplicated document", document);
console.log("add duplicated dcuemnt - evaluation key", evaluation_key);
let evaluation = state.evaluationsPicked.find(
(e) => e.key === evaluation_key,
);
document.key = evaluation.documents.length + 1;
evaluation.documents.splice(0, 0, document);
},
addDuplicatedDocumentToEvaluation(state, { document, evaluation }) {
let evaluationDest = state.evaluationsPicked.find(
(e) => e.id === evaluation.id,
);
if (evaluationDest) {
console.log("add duplicated document to evaluation", evaluationDest);
document.key = evaluationDest.documents.length + 1;
evaluationDest.documents.splice(0, 0, document);
}
},
moveDocumentToEvaluation(
state,
{ evaluationInitial, evaluationDest, document },
) {
let evaluationA = state.evaluationsPicked.find(
(e) => e.id === evaluationInitial.id,
);
let evaluationB = state.evaluationsPicked.find(
(e) => e.id === evaluationDest.id,
);
if (evaluationB) {
// add document to chosen evaluation if evaluation is part of the same social work
document.key = evaluationB.documents.length + 1;
evaluationB.documents.splice(0, 0, document);
}
// remove document from original evaluation
const indexToRemove = evaluationA.documents.findIndex(
(doc) => doc.id === document.id,
);
if (indexToRemove !== -1) {
evaluationA.documents.splice(indexToRemove, 1);
}
},
/**
* Replaces a document in the state with a new document.
*
@@ -603,6 +636,44 @@ const store = createStore({
const newDoc = await duplicate(document.id);
commit("addDuplicatedDocument", { document: newDoc, evaluation_key });
},
async duplicateDocumentToEvaluation({ commit }, { document, evaluation }) {
try {
const newDoc = await duplicateDocumentToEvaluation(
document.id,
evaluation.id,
);
commit("addDuplicatedDocumentToEvaluation", {
document: newDoc,
evaluation,
});
return newDoc;
} catch (error) {
console.error("Failed to move document:", error);
throw error;
}
},
async moveDocumentToEvaluation(
{ commit },
{ evaluationInitial, evaluationDest, document },
) {
try {
const response = await moveDocumentToEvaluation(
document.id,
evaluationDest.id,
);
commit("moveDocumentToEvaluation", {
evaluationInitial,
evaluationDest,
document,
});
return response;
} catch (error) {
console.error("Failed to move document:", error);
throw error;
}
},
removeDocument({ commit }, payload) {
commit("removeDocument", payload);
},

View File

@@ -9,3 +9,23 @@ export const duplicate = async (
`/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`,
);
};
export const duplicateDocumentToEvaluation = async (
document_id: number,
evaluation_id: number,
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
"POST",
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/duplicate`,
);
};
export const moveDocumentToEvaluation = async (
document_id: number,
evaluation_id: number,
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
"POST",
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/move`,
);
};

View File

@@ -0,0 +1,70 @@
<template>
<div class="container">
<div class="item-bloc">
<div class="item-row">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
<span>
{{ trans(EVALUATION) }}:
<span class="badge bg-light text-dark">
{{ eval?.evaluation?.title.fr }}
</span>
</span>
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">
{{
trans(
ACCOMPANYING_COURSE_WORK_START_DATE,
)
}}
:
</span>
<b>{{ formatDate(eval.startDate) }}</b>
</li>
<li v-if="eval.endDate">
<span class="item-key">
{{
trans(ACCOMPANYING_COURSE_WORK_END_DATE)
}}
:
</span>
<b>{{ formatDate(eval.endDate) }}</b>
</li>
</ul>
</span>
</h2>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ACCOMPANYING_COURSE_WORK_END_DATE,
ACCOMPANYING_COURSE_WORK_START_DATE,
EVALUATION,
trans,
} from "translator";
import { ISOToDate } from "ChillMainAssets/chill/js/date";
import { DateTime } from "ChillMainAssets/types";
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{ eval: AccompanyingPeriodWorkEvaluation }>();
const formatDate = (dateObject: DateTime) => {
if (dateObject) {
const parsedDate = ISOToDate(dateObject.datetime);
if (parsedDate) {
return new Intl.DateTimeFormat("default", {
dateStyle: "short",
}).format(parsedDate);
} else {
return "";
}
}
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="results">
<div
v-for="evaluation in evaluations"
:key="evaluation.id"
class="list-item"
>
<label class="acpw-item">
<div>
<input
type="radio"
:value="evaluation"
v-model="selectedEvaluation"
name="item"
/>
</div>
<accompanying-period-work-evaluation-item :eval="evaluation" />
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
import { defineProps, ref, watch } from "vue";
import AccompanyingPeriodWorkEvaluationItem from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationItem.vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
evaluations: AccompanyingPeriodWorkEvaluation[];
}>();
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
watch(selectedEvaluation, (newValue) => {
emit("update:selectedEvaluation", newValue);
});
</script>
<style>
.acpw-item {
display: flex;
}
</style>

View File

@@ -26,14 +26,24 @@ import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
import { AccompanyingPeriodWork } from "../../../types";
import { defineProps, ref, watch } from "vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps<{
accompanyingPeriodWorks: AccompanyingPeriodWork[];
selectedAcpw?: AccompanyingPeriodWork | null;
}>();
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const selectedAcpw = ref<AccompanyingPeriodWork | null>(
props.selectedAcpw ?? null,
);
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
const emit = defineEmits<{
"update:selectedAcpw": [value: AccompanyingPeriodWork | null];
}>();
watch(
() => props.selectedAcpw,
(val) => {
selectedAcpw.value = val ?? null;
},
);
watch(selectedAcpw, (newValue) => {
emit("update:selectedAcpw", newValue);

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div class="row justify-content-end">
<div class="row justify-content-end" v-if="!isEvaluationSelector">
<div class="col-md-6 col-sm-10" v-if="selectedAcpw">
<ul class="list-suggest remove-items">
<li>
@@ -14,7 +14,7 @@
</div>
</div>
<ul class="record_actions">
<ul v-if="!showModal" class="record_actions">
<li>
<a class="btn btn-sm btn-create mt-3" @click="openModal">
{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
@@ -40,9 +40,15 @@
<template #body>
<accompanying-period-work-list
v-if="evaluations.length === 0"
:accompanying-period-works="accompanyingPeriodWorks"
v-model:selectedAcpw="selectedAcpw"
/>
<accompanying-period-work-evaluation-list
v-if="evaluations.length > 0"
:evaluations="evaluations"
v-model:selectedEvaluation="selectedEvaluation"
/>
</template>
<template #footer>
@@ -60,58 +66,109 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { ref, watch, onMounted } from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
import { AccompanyingPeriodWork } from "../../../types";
import {
trans,
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
CONFIRM,
trans,
} from "translator";
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
interface AccompanyingPeriodWorkSelectorModalProps {
accompanyingPeriodId: number;
}
import AccompanyingPeriodWorkEvaluationList from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationList.vue";
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
const showModal = ref(false);
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
const evaluations = ref<AccompanyingPeriodWorkEvaluation[]>([]);
const props = defineProps<{
accompanyingPeriodId: string;
isEvaluationSelector: boolean;
ignoreAccompanyingPeriodWorkIds: number[];
}>();
const emit = defineEmits<{
pickWork: [payload: { work: AccompanyingPeriodWork | null }];
closeModal: [];
"update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
}>();
onMounted(() => {
if (props.accompanyingPeriodId) {
getAccompanyingPeriodWorks(props.accompanyingPeriodId);
getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
} else {
console.error("No accompanyingperiod id was given");
}
showModal.value = true;
});
const getAccompanyingPeriodWorks = async (periodId: number) => {
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
try {
accompanyingPeriodWorks.value = await fetchResults(url);
} catch (error) {
console.log(error);
const accompanyingPeriodWorksFetched =
await fetchResults<AccompanyingPeriodWork>(url);
if (props.isEvaluationSelector) {
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched.filter(
(acpw: AccompanyingPeriodWork) =>
acpw.accompanyingPeriodWorkEvaluations.length > 0 &&
typeof acpw.id !== "undefined" &&
!props.ignoreAccompanyingPeriodWorkIds.includes(acpw.id),
);
} else {
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched;
}
/* makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
.then((response) => {
accompanyingPeriodWorks.value = response;
})
.catch((error) => {
console.log(error);
});*/
};
const openModal = () => (showModal.value = true);
const closeModal = () => (showModal.value = false);
watch(selectedAcpw, (newValue) => {
const inputField = document.getElementById(
"find_accompanying_period_work_acpw",
) as HTMLInputElement;
if (inputField) {
inputField.value = String(newValue?.id || "");
}
/* if (!props.isEvaluationSelector) {
console.log("Emitting from watch:", { work: newValue });
emit("pickWork", { work: newValue });
}*/
});
const openModal = () => {
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
selectedEvaluation.value = null;
// selectedAcpw.value = null;
emit("closeModal");
};
const confirmSelection = () => {
selectedAcpw.value = selectedAcpw.value;
console.log("selectedAcpw", selectedAcpw.value);
if (!props.isEvaluationSelector) {
if (selectedAcpw.value) {
// only emit if something is actually selected!
emit("pickWork", { work: selectedAcpw.value });
closeModal();
}
// optionally show some error or warning if not selected
return;
}
if (selectedAcpw.value && props.isEvaluationSelector) {
evaluations.value =
selectedAcpw.value.accompanyingPeriodWorkEvaluations;
}
if (selectedEvaluation.value && props.isEvaluationSelector) {
// console.log('evaluation log in modal', selectedEvaluation.value)
emit("update:selectedEvaluation", selectedEvaluation.value);
closeModal();
}
};
</script>

View File

@@ -1,9 +1,9 @@
{%- macro details(w, accompanyingCourse, options) -%}
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
'displayAction': false,
'displayAction': true,
'displayContent': 'short',
'displayFontSmall': true,
'itemBlocClass': '',
'displayNotification': false
'displayNotification': true
} %}
{% endmacro %}

View File

@@ -2,10 +2,10 @@
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
{% block content %}
<div class="person-duplicate">
@@ -18,7 +18,7 @@
</div>
</div>
<h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
<h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
{{ form_start(form) }}
{%- if form.acpw is defined -%}
{{ form_row(form.acpw) }}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -36,4 +37,17 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicator
return $newDocument;
}
public function duplicateToEvaluation(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): AccompanyingPeriodWorkEvaluationDocument
{
$newDocument = new AccompanyingPeriodWorkEvaluationDocument();
$newDocument
->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
;
$evaluation->addDocument($newDocument);
return $newDocument;
}
}

View File

@@ -1993,3 +1993,33 @@ paths:
application/json:
schema:
type: object
/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
post:
tags:
- accompanying-course-work-evaluation-document
summary: Dupliate an an accompanying period work evaluation document to another evaluation
parameters:
- in: path
name: document_id
required: true
description: The document's id
schema:
type: integer
format: integer
minimum: 1
- in: path
name: evaluation_id
required: true
description: The evaluation's id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "OK"
content:
application/json:
schema:
type: object

View File

@@ -753,6 +753,42 @@ evaluation:
delay: Délai
notificationDelay: Délai de notification
url: Lien internet
title: Ecrire une évaluation
status: Statut
choose_a_status: Choisir un statut
startdate: Date d'ouverture
enddate: Date de fin
maxdate: Date d'échéance
warning_interval: Rappel (jours)
public_comment: Note publique
comment_placeholder: Commencez à écrire ...
generate_a_document: Générer un document
choose_a_template: Choisir un modèle
add_a_document: Ajouter un document
add: Ajouter une évaluation
time_spent: Temps de rédaction
select_time_spent: Indiquez le temps de rédaction
Documents: Documents
document_add: Générer ou téléverser un document
document_upload: Téléverser un document
document_title: Titre du document
template_title: Nom du template
browse: Ajouter un document
replace: Remplacer
download: Télécharger le fichier existant
notification_notify_referrer: Notifier le référent
notification_notify_any: Notifier d'autres utilisateurs
notification_send: Envoyer une notification
document:
edit: Modifier
delete: Supprimer
move: Déplacer
duplicate: Dupliquer
duplicate_here: Dupliquer ici
duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
duplicate_success: Le document d'évaluation a été dupliquer
move_success: Le document d'évaluation a été déplacer
goal:
desactivationDate: Date de désactivation
@@ -777,7 +813,6 @@ relation:
reverseTitle: Deuxième membre
days: jours
months: mois
years: années
# specific to closing motive
@@ -1525,3 +1560,6 @@ my_parcours_filters:
parcours_intervening: Intervenant
is_open: Parcours ouverts
is_closed: Parcours clôturés
document_duplicate:
to_evaluation_success: "Le document a été dupliquer"