mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-20 05:35:00 +00:00
Compare commits
4 Commits
375-notifi
...
421-signat
Author | SHA1 | Date | |
---|---|---|---|
c4d5984b92 | |||
376497b704 | |||
f90e9ea3bc | |||
5465a6f336 |
6
.changes/unreleased/Feature-20250724-161556.yaml
Normal file
6
.changes/unreleased/Feature-20250724-161556.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Only allow delete of attachment on workflows that are not final
|
||||
time: 2025-07-24T16:15:56.042884578+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
6
.changes/unreleased/Feature-20250724-161628.yaml
Normal file
6
.changes/unreleased/Feature-20250724-161628.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Move up signature buttons on index workflow page for easier access
|
||||
time: 2025-07-24T16:16:28.609598883+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
6
.changes/unreleased/Feature-20250724-172013.yaml
Normal file
6
.changes/unreleased/Feature-20250724-172013.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
kind: Feature
|
||||
body: Filter out document from attachment list if it is the same as the workflow document
|
||||
time: 2025-07-24T17:20:13.118537573+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: adjust display logic for accompanying period dates, include closing date if period is closed.
|
||||
time: 2025-08-06T13:46:09.241584292+02:00
|
||||
custom:
|
||||
Issue: "382"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: add min and step attributes to integer field in DateIntervalType
|
||||
time: 2025-08-06T17:35:27.413787704+02:00
|
||||
custom:
|
||||
Issue: "384"
|
||||
SchemaChange: No schema change
|
@@ -1,5 +1,6 @@
|
||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import {EntityWorkflow} from "ChillMainAssets/types";
|
||||
|
||||
export function fetch_generic_docs_by_accompanying_period(
|
||||
periodId: number,
|
||||
@@ -8,3 +9,17 @@ export function fetch_generic_docs_by_accompanying_period(
|
||||
`/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`,
|
||||
);
|
||||
}
|
||||
|
||||
export const fetchWorkflow = async (
|
||||
workflowId: number,
|
||||
): Promise<EntityWorkflow> => {
|
||||
try {
|
||||
return await makeFetch<null, EntityWorkflow>(
|
||||
"GET",
|
||||
`/api/1.0/main/workflow/${workflowId}.json`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch workflow ${workflowId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@@ -25,7 +25,7 @@ export interface GenericDoc {
|
||||
type: "doc_store_generic_doc";
|
||||
uniqueKey: string;
|
||||
key: string;
|
||||
identifiers: object;
|
||||
identifiers: { id: number; };
|
||||
context: "person" | "accompanying-period";
|
||||
doc_date: DateTime;
|
||||
metadata: GenericDocMetadata;
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class WorkflowApiController
|
||||
class WorkflowApiController extends ApiController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly PaginatorFactory $paginatorFactory, private readonly Security $security, private readonly SerializerInterface $serializer) {}
|
||||
|
||||
|
@@ -30,6 +30,7 @@ use Chill\MainBundle\Controller\UserGroupAdminController;
|
||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||
use Chill\MainBundle\Controller\UserJobApiController;
|
||||
use Chill\MainBundle\Controller\UserJobController;
|
||||
use Chill\MainBundle\Controller\WorkflowApiController;
|
||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||
use Chill\MainBundle\Doctrine\DQL\Age;
|
||||
use Chill\MainBundle\Doctrine\DQL\Extract;
|
||||
@@ -66,6 +67,7 @@ use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Form\CenterType;
|
||||
use Chill\MainBundle\Form\CivilityType;
|
||||
use Chill\MainBundle\Form\CountryType;
|
||||
@@ -79,6 +81,7 @@ use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
use Ramsey\Uuid\Doctrine\UuidType;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
@@ -940,6 +943,21 @@ class ChillMainExtension extends Extension implements
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => EntityWorkflow::class,
|
||||
'name' => 'workflow',
|
||||
'base_path' => '/api/1.0/main/workflow',
|
||||
'base_role' => EntityWorkflowVoter::SEE,
|
||||
'controller' => WorkflowApiController::class,
|
||||
'actions' => [
|
||||
'_entity' => [
|
||||
'methods' => [
|
||||
Request::METHOD_GET => true,
|
||||
Request::METHOD_HEAD => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@@ -55,10 +55,6 @@ class DateIntervalType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('n', IntegerType::class, [
|
||||
'attr' => [
|
||||
'min' => 0,
|
||||
'step' => 1,
|
||||
],
|
||||
'constraints' => [
|
||||
new GreaterThan([
|
||||
'value' => 0,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import {Person} from "../../../ChillPersonBundle/Resources/public/types";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
@@ -202,6 +203,55 @@ export interface WorkflowAttachment {
|
||||
genericDoc: null | GenericDoc;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface EntityWorkflowStep {
|
||||
type: "entity_workflow_step";
|
||||
id: number;
|
||||
comment: string;
|
||||
currentStep: StepDefinition;
|
||||
isFinal: boolean;
|
||||
isFreezed: boolean;
|
||||
isFinalized: boolean;
|
||||
transitionPrevious: Transition | null;
|
||||
transitionAfter: Transition | null;
|
||||
previousId: number | null;
|
||||
nextId: number | null;
|
||||
transitionPreviousBy: User | null;
|
||||
transitionPreviousAt: DateTime | null;
|
||||
}
|
||||
|
||||
export interface Transition {
|
||||
name: string;
|
||||
text: string;
|
||||
isForward: boolean;
|
||||
}
|
||||
|
||||
export interface StepDefinition {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface EntityWorkflow {
|
||||
type: "entity_workflow";
|
||||
id: number;
|
||||
relatedEntityClass: string;
|
||||
relatedEntityId: number;
|
||||
workflow: Workflow;
|
||||
currentStep: EntityWorkflowStep;
|
||||
steps: EntityWorkflowStep[];
|
||||
datas: WorkflowData;
|
||||
title: string;
|
||||
isOnHoldAtCurrentStep: boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowData {
|
||||
persons: Person[];
|
||||
}
|
||||
|
||||
export interface ExportGeneration {
|
||||
id: string;
|
||||
type: "export_generation";
|
||||
|
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useTemplateRef } from "vue";
|
||||
import type { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import {computed, onMounted, ref, useTemplateRef} from "vue";
|
||||
import type {EntityWorkflow, WorkflowAttachment} from "ChillMainAssets/types";
|
||||
import PickGenericDocModal from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import AttachmentList from "ChillMainAssets/vuejs/WorkflowAttachment/Component/AttachmentList.vue";
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types";
|
||||
import {fetch_generic_docs_by_accompanying_period, fetchWorkflow} from "ChillDocStoreAssets/js/generic-doc-api";
|
||||
|
||||
interface AppConfig {
|
||||
workflowId: number;
|
||||
@@ -34,6 +35,13 @@ const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
|
||||
) as GenericDocForAccompanyingPeriod[],
|
||||
);
|
||||
|
||||
const workflow = ref<EntityWorkflow | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
workflow.value = await fetchWorkflow(Number(props.workflowId));
|
||||
console.log('workflow', workflow.value);
|
||||
});
|
||||
|
||||
const openModal = function () {
|
||||
pickDocModal.value?.openModal();
|
||||
};
|
||||
@@ -53,12 +61,14 @@ const onRemoveAttachment = (payload: { attachment: WorkflowAttachment }) => {
|
||||
|
||||
<template>
|
||||
<pick-generic-doc-modal
|
||||
:workflow="workflow"
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="attachedGenericDoc"
|
||||
ref="pickDocModal"
|
||||
@pickGenericDoc="onPickGenericDoc"
|
||||
></pick-generic-doc-modal>
|
||||
<attachment-list
|
||||
:workflow="workflow"
|
||||
:attachments="props.attachments"
|
||||
@removeAttachment="onRemoveAttachment"
|
||||
></attachment-list>
|
||||
|
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import {EntityWorkflow, WorkflowAttachment} from "ChillMainAssets/types";
|
||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface AttachmentListProps {
|
||||
attachments: WorkflowAttachment[];
|
||||
workflow: EntityWorkflow | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -36,7 +37,7 @@ const props = defineProps<AttachmentListProps>();
|
||||
:stored-object="a.genericDoc.storedObject"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="!workflow?.currentStep.isFinal">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-delete"
|
||||
|
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
GenericDoc,
|
||||
GenericDocForAccompanyingPeriod,
|
||||
} from "ChillDocStoreAssets/types/generic_doc";
|
||||
import {GenericDoc, GenericDocForAccompanyingPeriod,} from "ChillDocStoreAssets/types/generic_doc";
|
||||
import PickGenericDocItem from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue";
|
||||
import { fetch_generic_docs_by_accompanying_period } from "ChillDocStoreAssets/js/generic-doc-api";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import {fetch_generic_docs_by_accompanying_period} from "ChillDocStoreAssets/js/generic-doc-api";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {EntityWorkflow} from "ChillMainAssets/types";
|
||||
|
||||
interface PickGenericDocProps {
|
||||
workflow: EntityWorkflow | null;
|
||||
accompanyingPeriodId: number;
|
||||
pickedList: GenericDocForAccompanyingPeriod[];
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
@@ -36,9 +35,19 @@ const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
|
||||
) !== -1;
|
||||
|
||||
onMounted(async () => {
|
||||
genericDocs.value = await fetch_generic_docs_by_accompanying_period(
|
||||
const fetchedGenericDocs = await fetch_generic_docs_by_accompanying_period(
|
||||
props.accompanyingPeriodId,
|
||||
);
|
||||
const documentClasses = [
|
||||
"Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument",
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument",
|
||||
"Chill\\DocStoreBundle\\Entity\\PersonDocument"
|
||||
];
|
||||
|
||||
genericDocs.value = fetchedGenericDocs.filter(doc =>
|
||||
!documentClasses.includes(props.workflow?.relatedEntityClass || '') ||
|
||||
props.workflow?.relatedEntityId !== doc.identifiers.id
|
||||
);
|
||||
loaded.value = true;
|
||||
});
|
||||
|
||||
|
@@ -3,8 +3,10 @@ import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import { computed, ref, useTemplateRef } from "vue";
|
||||
import PickGenericDoc from "ChillMainAssets/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import {EntityWorkflow} from "ChillMainAssets/types";
|
||||
|
||||
interface PickGenericDocModalProps {
|
||||
workflow: EntityWorkflow | null;
|
||||
accompanyingPeriodId: number;
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
}
|
||||
@@ -80,6 +82,7 @@ defineExpose({ openModal, closeModal });
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<pick-generic-doc
|
||||
:workflow="props.workflow"
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="props.toRemove"
|
||||
:picked-list="pickeds"
|
||||
|
@@ -58,12 +58,14 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if signatures|length > 0 %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_attachment.html.twig' %}</section>
|
||||
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||
{% if signatures|length > 0 %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||
{% if entity_workflow.currentStep.sends|length > 0 %}
|
||||
<section class="step my-4">
|
||||
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||
|
@@ -965,6 +965,31 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserJob"
|
||||
/1.0/main/workflow/{id}.json:
|
||||
get:
|
||||
tags:
|
||||
- workflow
|
||||
summary: Return a workflow
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The workflow id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
200:
|
||||
description: "ok"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
404:
|
||||
description: "not found"
|
||||
401:
|
||||
description: "Unauthorized"
|
||||
/1.0/main/workflow/my:
|
||||
get:
|
||||
tags:
|
||||
|
@@ -32,16 +32,9 @@
|
||||
<div class="wl-col list">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
{% if app != null %}
|
||||
{% if acp.closingDate != null %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
||||
'%opening_date%': acp.openingDate|format_date('long'),
|
||||
'%closing_date%': acp.closingDate|format_date('long')}
|
||||
) }}
|
||||
{% else %}
|
||||
<div class="date">
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="date">
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %}
|
||||
@@ -77,20 +70,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if acp.step == 'CLOSED' and acp.closingMotive is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3 class="closingMotive">{{ 'Closing motive'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<div>
|
||||
{{ acp.closingMotive.name|localize_translatable_string }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if acp.user is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
|
@@ -13,6 +13,7 @@ namespace Chill\TaskBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Chill\MainBundle\Serializer\Model\Counter;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
@@ -22,7 +23,6 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Event\AssignTaskEvent;
|
||||
use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\TaskBundle\Form\SingleTaskType;
|
||||
@@ -48,6 +48,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
final class SingleTaskController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CenterResolverDispatcherInterface $centerResolverDispatcher,
|
||||
private readonly PaginatorFactory $paginatorFactory,
|
||||
private readonly SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
@@ -168,9 +169,6 @@ final class SingleTaskController extends AbstractController
|
||||
->setForm($this->setCreateForm($task, TaskVoter::UPDATE));
|
||||
$this->eventDispatcher->dispatch($event, UIEvent::EDIT_FORM);
|
||||
|
||||
// To keep track of specific assignee change
|
||||
$initialAssignee = $task->getAssignee();
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
$form->handleRequest($request);
|
||||
@@ -180,13 +178,6 @@ final class SingleTaskController extends AbstractController
|
||||
$em = $this->managerRegistry->getManager();
|
||||
$em->persist($task);
|
||||
|
||||
if (null !== $task->getAssignee()) {
|
||||
$this->eventDispatcher->dispatch(
|
||||
new AssignTaskEvent($task, $initialAssignee),
|
||||
AssignTaskEvent::PERSIST
|
||||
);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator
|
||||
@@ -534,13 +525,6 @@ final class SingleTaskController extends AbstractController
|
||||
|
||||
$this->eventDispatcher->dispatch(new TaskEvent($task), TaskEvent::PERSIST);
|
||||
|
||||
if (null !== $task->getAssignee()) {
|
||||
$this->eventDispatcher->dispatch(
|
||||
new AssignTaskEvent($task, null),
|
||||
AssignTaskEvent::PERSIST
|
||||
);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('The task is created'));
|
||||
@@ -640,7 +624,8 @@ final class SingleTaskController extends AbstractController
|
||||
->addCheckbox('status', $statuses, $statuses, $statusTrans);
|
||||
|
||||
$states = $this->singleTaskStateRepository->findAllExistingStates();
|
||||
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['to_validate', 'in_progress', 'closed', 'canceled', 'validated'], true)));
|
||||
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['in_progress', 'closed', 'canceled', 'validated'], true)));
|
||||
|
||||
if ([] !== $states) {
|
||||
$filterBuilder
|
||||
->addCheckbox('states', $states, $checked);
|
||||
|
@@ -42,7 +42,6 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface
|
||||
$loader->load('services/timeline.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
$loader->load('services/form.yaml');
|
||||
$loader->load('services/notification.yaml');
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
@@ -1,41 +0,0 @@
|
||||
<?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\TaskBundle\Event;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class AssignTaskEvent extends Event
|
||||
{
|
||||
final public const PERSIST = 'chill_task.assign_task';
|
||||
|
||||
public function __construct(
|
||||
private readonly SingleTask $task,
|
||||
private readonly ?User $initialAssignee,
|
||||
) {}
|
||||
|
||||
public function getTask(): SingleTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function getInitialAssignee(): ?User
|
||||
{
|
||||
return $this->initialAssignee;
|
||||
}
|
||||
|
||||
public function hasAssigneeChanged(): bool
|
||||
{
|
||||
return $this->initialAssignee !== $this->task->getAssignee();
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
<?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\TaskBundle\Event;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
readonly class TaskAssignEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private \Twig\Environment $engine,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AssignTaskEvent::PERSIST => ['onTaskAssigned', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification when a user is assigned to a task.
|
||||
* Only triggers when the assignee actually changes.
|
||||
*/
|
||||
public function onTaskAssigned(AssignTaskEvent $event): void
|
||||
{
|
||||
if (!$event->hasAssigneeChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$task = $event->getTask();
|
||||
$assignedUser = $task->getAssignee();
|
||||
|
||||
$title = $task->getTitle();
|
||||
|
||||
$context = [
|
||||
'task' => $task,
|
||||
'assignedUser' => $assignedUser,
|
||||
'title' => $title,
|
||||
];
|
||||
|
||||
$notification = new Notification();
|
||||
$notification
|
||||
->setRelatedEntityId($task->getId())
|
||||
->setRelatedEntityClass(SingleTask::class)
|
||||
->setTitle($this->engine->render('@ChillTask/Notification/task_assignment_notification_title.txt.twig', $context))
|
||||
->setMessage($this->engine->render('@ChillTask/Notification/task_assignment_notification_content.txt.twig', $context))
|
||||
->addAddressee($assignedUser)
|
||||
->setType(AssignTaskNotificationFlagProvider::FLAG);
|
||||
|
||||
$this->entityManager->persist($notification);
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
<?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\TaskBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class AssignTaskNotificationFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'task-assign-notif';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.task_assign');
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
<?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\TaskBundle\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Repository\SingleTaskRepository;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
final readonly class TaskNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
public function __construct(private SingleTaskRepository $taskRepository) {}
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return '@ChillTask/SingleTask/showInNotification.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
return [
|
||||
'notification' => $notification,
|
||||
'task' => $this->taskRepository->find($notification->getRelatedEntityId()),
|
||||
];
|
||||
}
|
||||
|
||||
public function supports(Notification $notification, array $options = []): bool
|
||||
{
|
||||
return SingleTask::class === $notification->getRelatedEntityClass();
|
||||
}
|
||||
|
||||
public function getTitle(Notification $notification, array $options = []): TranslatableInterface
|
||||
{
|
||||
if (null === $task = $this->getRelatedEntity($notification)) {
|
||||
return new TranslatableMessage('task.deleted');
|
||||
}
|
||||
|
||||
return new TranslatableMessage('notification.task.title %title%', ['title' => $task->getTitle()]);
|
||||
}
|
||||
|
||||
public function getAssociatedPersons(Notification $notification, array $options = []): array
|
||||
{
|
||||
if (null === $task = $this->getRelatedEntity($notification)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (null !== $task->getCourse()) {
|
||||
return $task->getCourse()->getParticipations()->getValues();
|
||||
}
|
||||
|
||||
return [$task->getPerson()];
|
||||
}
|
||||
|
||||
public function getRelatedEntity(Notification $notification): ?object
|
||||
{
|
||||
return $this->taskRepository->find($notification->getRelatedEntityId());
|
||||
}
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
{{ assignedUser.label }},
|
||||
|
||||
{{ 'notification.email.task_assigned'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
{{ 'notification.email.title_label'|trans({}, null, assignedUser.getLocale) }} "{{ task.title }}".
|
||||
{% if task.endDate %}
|
||||
|
||||
{{ 'notification.email.deadline'|trans({'%date%': task.endDate|format_date('long')}, null, assignedUser.getLocale) }}
|
||||
{% endif %}
|
||||
|
||||
{{ 'notification.email.view_task'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
{{ absolute_url(path('chill_task_single_task_show', {'id': task.id, '_locale': assignedUser.getLocale})) }}
|
||||
|
||||
{{ 'notification.email.regards'|trans({}, null, assignedUser.getLocale) }},
|
@@ -1,3 +0,0 @@
|
||||
{{ 'notification.email.title'|trans({}, null, assignedUser.getLocale) }}
|
||||
|
||||
|
@@ -18,14 +18,14 @@
|
||||
<div>
|
||||
{% if task.person is not null %}
|
||||
<span class="chill-task-list__row__person">
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: task.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: task.person|chill_entity_render_string,
|
||||
isDead: task.person.deathdate is not null
|
||||
} %}
|
||||
</span>
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: task.person.id },
|
||||
action: 'show',
|
||||
displayBadge: true,
|
||||
buttonText: task.person|chill_entity_render_string,
|
||||
isDead: task.person.deathdate is not null
|
||||
} %}
|
||||
</span>
|
||||
{% elseif task.course is not null %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
{% for part in task.course.currentParticipations %}
|
||||
|
@@ -110,5 +110,4 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</ul></div>
|
||||
|
@@ -1,14 +0,0 @@
|
||||
{% macro recordAction(task) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_person_accompanying_course_index', { 'task_id': task }) }}"
|
||||
class="btn btn-show" title="{{ 'See task'|trans }}"></a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% if task is not null %}
|
||||
{# <div>Todo : display task? </div>#}
|
||||
{% else %}
|
||||
<div class="alert alert-warning border-warning border-1">
|
||||
{{ 'You are getting a notification for a task which does not exist any more'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
@@ -1,131 +0,0 @@
|
||||
<?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\TaskBundle\Tests\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\TaskBundle\Entity\SingleTask;
|
||||
use Chill\TaskBundle\Event\AssignTaskEvent;
|
||||
use Chill\TaskBundle\Event\TaskAssignEventSubscriber;
|
||||
use Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TaskAssignEventSubscriberTest extends TestCase
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private Environment $twig;
|
||||
private TaskAssignEventSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$this->twig = $this->createMock(Environment::class);
|
||||
$this->subscriber = new TaskAssignEventSubscriber($this->entityManager, $this->twig);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$events = TaskAssignEventSubscriber::getSubscribedEvents();
|
||||
|
||||
$this->assertArrayHasKey(AssignTaskEvent::PERSIST, $events);
|
||||
$this->assertEquals(['onTaskAssigned', 0], $events[AssignTaskEvent::PERSIST]);
|
||||
}
|
||||
|
||||
public function testOnTaskAssignedCreatesNotificationWhenAssigneeChanges(): void
|
||||
{
|
||||
// Arrange
|
||||
$task = $this->createMock(SingleTask::class);
|
||||
$assignee = $this->createMock(User::class);
|
||||
$event = $this->createMock(AssignTaskEvent::class);
|
||||
|
||||
$task->method('getId')->willReturn(123);
|
||||
$task->method('getTitle')->willReturn('Test Task');
|
||||
$task->method('getAssignee')->willReturn($assignee);
|
||||
|
||||
$event->method('hasAssigneeChanged')->willReturn(true);
|
||||
$event->method('getTask')->willReturn($task);
|
||||
|
||||
$this->twig->expects($this->exactly(2))
|
||||
->method('render')
|
||||
->with(
|
||||
$this->logicalOr(
|
||||
'@ChillTask/Notification/task_assignment_notification_title.txt.twig',
|
||||
'@ChillTask/Notification/task_assignment_notification_content.txt.twig'
|
||||
),
|
||||
$this->isType('array')
|
||||
)
|
||||
->willReturnOnConsecutiveCalls('Notification Title', 'Notification Content');
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('persist')
|
||||
->with($this->isInstanceOf(Notification::class));
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
}
|
||||
|
||||
public function testOnTaskAssignedDoesNothingWhenAssigneeDoesNotChange(): void
|
||||
{
|
||||
// Arrange
|
||||
$event = $this->createMock(AssignTaskEvent::class);
|
||||
$event->method('hasAssigneeChanged')->willReturn(false);
|
||||
|
||||
$this->twig->expects($this->never())->method('render');
|
||||
$this->entityManager->expects($this->never())->method('persist');
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
}
|
||||
|
||||
public function testNotificationHasCorrectProperties(): void
|
||||
{
|
||||
// Arrange
|
||||
$task = $this->createMock(SingleTask::class);
|
||||
$assignee = $this->createMock(User::class);
|
||||
$event = $this->createMock(AssignTaskEvent::class);
|
||||
|
||||
$task->method('getId')->willReturn(456);
|
||||
$task->method('getTitle')->willReturn('Important Task');
|
||||
$task->method('getAssignee')->willReturn($assignee);
|
||||
|
||||
$event->method('hasAssigneeChanged')->willReturn(true);
|
||||
$event->method('getTask')->willReturn($task);
|
||||
|
||||
$this->twig->method('render')->willReturn('Test Content');
|
||||
|
||||
// Capture the persisted notification
|
||||
$persistedNotification = null;
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('persist')
|
||||
->willReturnCallback(function ($notification) use (&$persistedNotification) {
|
||||
$persistedNotification = $notification;
|
||||
});
|
||||
|
||||
// Act
|
||||
$this->subscriber->onTaskAssigned($event);
|
||||
|
||||
// Assert
|
||||
$this->assertInstanceOf(Notification::class, $persistedNotification);
|
||||
$this->assertEquals(456, $persistedNotification->getRelatedEntityId());
|
||||
$this->assertEquals(SingleTask::class, $persistedNotification->getRelatedEntityClass());
|
||||
$this->assertEquals(AssignTaskNotificationFlagProvider::FLAG, $persistedNotification->getType());
|
||||
$this->assertEquals('Test Content', $persistedNotification->getTitle());
|
||||
$this->assertEquals('Test Content', $persistedNotification->getMessage());
|
||||
}
|
||||
}
|
@@ -1,13 +1,7 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\TaskBundle\Event\Lifecycle\TaskLifecycleEvent:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
||||
|
||||
Chill\TaskBundle\Event\TaskAssignEventSubscriber: ~
|
||||
|
@@ -1,7 +0,0 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\TaskBundle\Notification\TaskNotificationHandler: ~
|
||||
Chill\TaskBundle\Notification\AssignTaskNotificationFlagProvider: ~
|
@@ -116,16 +116,3 @@ CHILL_TASK_TASK_UPDATE: Modifier une tâche
|
||||
CHILL_TASK_TASK_CREATE_FOR_COURSE: Créer une tâche pour un parcours
|
||||
CHILL_TASK_TASK_CREATE_FOR_PERSON: Créer une tâche pour un usager
|
||||
|
||||
notification:
|
||||
task:
|
||||
title %title%: "Tâche: title"
|
||||
flags:
|
||||
task_assign: Lorsqu'un autre utilisateur m'assigne à une tâche.
|
||||
email:
|
||||
title: "Une tâche demande votre attention"
|
||||
task_assigned: "Une tâche vous a été assignée."
|
||||
title_label: "Titre de la tâche:"
|
||||
deadline: "Vous êtes invités à accomplir cette tâche avant le %date%"
|
||||
view_task: "Vous pouvez visualiser la tâche sur cette page:"
|
||||
regards: "Cordialement"
|
||||
|
||||
|
Reference in New Issue
Block a user