mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-02 04:53:49 +00:00
Add attachments to workflow
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentAction;
|
||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentRequestDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
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 Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class WorkflowAttachmentController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly AddAttachmentAction $addAttachmentAction,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['POST'])]
|
||||
public function addAttachment(EntityWorkflow $entityWorkflow, Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$dto = new AddAttachmentRequestDTO($entityWorkflow);
|
||||
$this->serializer->deserialize($request->getContent(), AddAttachmentRequestDTO::class, 'json', [
|
||||
AbstractNormalizer::OBJECT_TO_POPULATE => $dto, AbstractNormalizer::GROUPS => ['write'],
|
||||
]);
|
||||
|
||||
$errors = $this->validator->validate($dto);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($errors, 'json'),
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
json: true
|
||||
);
|
||||
}
|
||||
|
||||
$attachment = ($this->addAttachmentAction)($dto);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($attachment, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/main/workflow/attachment/{id}', methods: ['DELETE'])]
|
||||
public function removeAttachment(EntityWorkflowAttachment $attachment): Response
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $attachment->getEntityWorkflow())) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$this->entityManager->remove($attachment);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new Response(null, Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['GET'])]
|
||||
public function listAttachmentsForEntityWorkflow(EntityWorkflow $entityWorkflow): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
$entityWorkflow->getAttachments(),
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']]
|
||||
),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -351,6 +351,7 @@ class WorkflowController extends AbstractController
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'transition_form_errors' => $errors,
|
||||
'signatures' => $signatures,
|
||||
'related_accompanying_period' => $this->entityWorkflowManager->getRelatedAccompanyingPeriod($entityWorkflow),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@@ -87,12 +87,19 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
||||
private string $workflowName;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowAttachment>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)]
|
||||
private Collection $attachments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->subscriberToFinal = new ArrayCollection();
|
||||
$this->subscriberToStep = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->steps = new ArrayCollection();
|
||||
$this->attachments = new ArrayCollection();
|
||||
|
||||
$initialStep = new EntityWorkflowStep();
|
||||
$initialStep
|
||||
@@ -142,6 +149,35 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal use @{EntityWorkflowAttachement::__construct} instead
|
||||
*/
|
||||
public function addAttachment(EntityWorkflowAttachment $attachment): self
|
||||
{
|
||||
if (!$this->attachments->contains($attachment)) {
|
||||
$this->attachments[] = $attachment;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EntityWorkflowAttachment>
|
||||
*/
|
||||
public function getAttachments(): Collection
|
||||
{
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
public function removeAttachment(EntityWorkflowAttachment $attachment): self
|
||||
{
|
||||
$this->attachments->removeElement($attachment);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
@@ -356,6 +392,17 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this->getCurrentStep()->isOnHoldByUser($user);
|
||||
}
|
||||
|
||||
public function isUserInvolved(User $user): bool
|
||||
{
|
||||
foreach ($this->getSteps() as $step) {
|
||||
if ($step->getAllDestUser()->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isUserSubscribedToFinal(User $user): bool
|
||||
{
|
||||
return $this->subscriberToFinal->contains($user);
|
||||
@@ -420,7 +467,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Method use by marking store.
|
||||
* Method used by marking store.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
|
@@ -0,0 +1,80 @@
|
||||
<?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\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity()]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_attachment')]
|
||||
#[ORM\UniqueConstraint(name: 'unique_generic_doc_by_workflow', columns: ['relatedGenericDocKey', 'relatedGenericDocIdentifiers', 'entityworkflow_id'])]
|
||||
class EntityWorkflowAttachment implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(name: 'relatedGenericDocKey', type: Types::STRING, length: 255, nullable: false)]
|
||||
private string $relatedGenericDocKey,
|
||||
#[ORM\Column(name: 'relatedGenericDocIdentifiers', type: Types::JSON, nullable: false, options: ['jsonb' => true])]
|
||||
private array $relatedGenericDocIdentifiers,
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'attachments')]
|
||||
#[ORM\JoinColumn(nullable: false, name: 'entityworkflow_id')]
|
||||
private EntityWorkflow $entityWorkflow,
|
||||
|
||||
/**
|
||||
* Stored object related to the generic doc.
|
||||
*
|
||||
* This is a story to keep track more easily to stored object
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class)]
|
||||
#[ORM\JoinColumn(nullable: false, name: 'storedobject_id')]
|
||||
private StoredObject $proxyStoredObject,
|
||||
) {
|
||||
$this->entityWorkflow->addAttachment($this);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEntityWorkflow(): EntityWorkflow
|
||||
{
|
||||
return $this->entityWorkflow;
|
||||
}
|
||||
|
||||
public function getRelatedGenericDocIdentifiers(): array
|
||||
{
|
||||
return $this->relatedGenericDocIdentifiers;
|
||||
}
|
||||
|
||||
public function getRelatedGenericDocKey(): string
|
||||
{
|
||||
return $this->relatedGenericDocKey;
|
||||
}
|
||||
|
||||
public function getProxyStoredObject(): StoredObject
|
||||
{
|
||||
return $this->proxyStoredObject;
|
||||
}
|
||||
}
|
@@ -17,6 +17,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Contains comment for entity workflow.
|
||||
*
|
||||
* **NOTE**: for now, this class is not in used. Comments are, for now, stored in the EntityWorkflowStep.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_main_workflow_entity_comment')]
|
||||
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
@@ -16,9 +16,18 @@ use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* A step for each EntityWorkflow.
|
||||
*
|
||||
* The step contains the history of position. The current one is the one which transitionAt or transitionAfter is NULL.
|
||||
*
|
||||
* The comments field is populated by the comment of the one who apply the transition, it means that the comment for the
|
||||
* "next" step is stored in the EntityWorkflowStep in the previous step.
|
||||
*
|
||||
* DestUsers are the one added at the transition. DestUserByAccessKey are the users who obtained permission after having
|
||||
* clicked on a link to get access (email notification to groups).
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table('chill_main_workflow_entity_step')]
|
||||
class EntityWorkflowStep
|
||||
@@ -80,6 +89,11 @@ class EntityWorkflowStep
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* If this is the final step.
|
||||
*
|
||||
* This property is filled by a listener.
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
|
||||
private bool $isFinal = false;
|
||||
|
||||
@@ -254,6 +268,11 @@ class EntityWorkflowStep
|
||||
return $this->ccUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the comment from the one who apply the transition.
|
||||
*
|
||||
* It means that it must be saved when the user apply a transition.
|
||||
*/
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
@@ -346,6 +365,9 @@ class EntityWorkflowStep
|
||||
return $this->transitionByEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if this is the end of the EntityWorkflow
|
||||
*/
|
||||
public function isFinal(): bool
|
||||
{
|
||||
return $this->isFinal;
|
||||
@@ -367,6 +389,9 @@ class EntityWorkflowStep
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step
|
||||
*/
|
||||
public function isWaitingForTransition(): bool
|
||||
{
|
||||
if (null !== $this->transitionAfter) {
|
||||
@@ -506,26 +531,6 @@ class EntityWorkflowStep
|
||||
return $this->holdsOnStep;
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void
|
||||
{
|
||||
return;
|
||||
|
||||
if ($this->isFinalizeAfter()) {
|
||||
if (0 !== \count($this->getDestUser())) {
|
||||
$context->buildViolation('workflow.No dest users when the workflow is finalized')
|
||||
->atPath('finalizeAfter')
|
||||
->addViolation();
|
||||
}
|
||||
} else {
|
||||
if (0 === \count($this->getDestUser())) {
|
||||
$context->buildViolation('workflow.The next step must count at least one dest')
|
||||
->atPath('finalizeAfter')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addOnHold(EntityWorkflowStepHold $onHold): self
|
||||
{
|
||||
if (!$this->holdsOnStep->contains($onHold)) {
|
||||
|
@@ -53,6 +53,7 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowStep $step,
|
||||
User|Person $signer,
|
||||
) {
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<?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\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @implements ObjectRepository<EntityWorkflowAttachment>
|
||||
*/
|
||||
class EntityWorkflowAttachmentRepository implements ObjectRepository
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $registry)
|
||||
{
|
||||
$this->repository = $registry->getRepository(EntityWorkflowAttachment::class);
|
||||
}
|
||||
|
||||
public function find($id): ?EntityWorkflowAttachment
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria)
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<EntityWorkflowAttachment>
|
||||
*/
|
||||
public function findByStoredObject(StoredObject $storedObject): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('a');
|
||||
$qb->where('a.proxyStoredObject = :storedObject')->setParameter('storedObject', $storedObject);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return EntityWorkflowAttachment::class;
|
||||
}
|
||||
}
|
@@ -480,7 +480,7 @@ div.workflow {
|
||||
section.step {
|
||||
border: 1px solid $chill-l-gray;
|
||||
padding: 1em 2em;
|
||||
div.flex-table {
|
||||
> div.flex-table {
|
||||
margin: 1.5em -2em;
|
||||
}
|
||||
}
|
||||
|
@@ -83,6 +83,10 @@ export const makeFetch = <Input, Output>(
|
||||
opts = Object.assign(opts, options);
|
||||
}
|
||||
return fetch(url, opts).then((response) => {
|
||||
if (response.status === 204) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
@@ -173,18 +177,26 @@ function _fetchAction<T>(
|
||||
|
||||
throw new Error("other network error");
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
console.error(reason);
|
||||
throw new Error(reason);
|
||||
});
|
||||
.catch(
|
||||
(
|
||||
reason:
|
||||
| NotFoundExceptionInterface
|
||||
| ServerExceptionInterface
|
||||
| ValidationExceptionInterface
|
||||
| TransportExceptionInterface,
|
||||
) => {
|
||||
console.error(reason);
|
||||
throw reason;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const fetchResults = async <T>(
|
||||
uri: string,
|
||||
params?: FetchParams,
|
||||
): Promise<T[]> => {
|
||||
let promises: Promise<T[]>[] = [],
|
||||
page = 1;
|
||||
const promises: Promise<T[]>[] = [];
|
||||
let page = 1;
|
||||
const firstData: PaginationResponse<T> = (await _fetchAction(
|
||||
page,
|
||||
uri,
|
||||
@@ -229,6 +241,7 @@ const ValidationException = (
|
||||
return error;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const AccessException = (response: Response): AccessExceptionInterface => {
|
||||
const error = {} as AccessExceptionInterface;
|
||||
error.name = "AccessException";
|
||||
@@ -237,6 +250,7 @@ const AccessException = (response: Response): AccessExceptionInterface => {
|
||||
return error;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
|
||||
const error = {} as NotFoundExceptionInterface;
|
||||
error.name = "NotFoundException";
|
||||
@@ -257,6 +271,7 @@ const ServerException = (
|
||||
};
|
||||
|
||||
const ConflictHttpException = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
response: Response,
|
||||
): ConflictHttpExceptionInterface => {
|
||||
const error = {} as ConflictHttpExceptionInterface;
|
||||
|
@@ -0,0 +1,22 @@
|
||||
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
|
||||
export const find_attachments_by_workflow = async (
|
||||
workflowId: number,
|
||||
): Promise<WorkflowAttachment[]> =>
|
||||
makeFetch("GET", `/api/1.0/main/workflow/${workflowId}/attachment`);
|
||||
|
||||
export const create_attachment = async (
|
||||
workflowId: number,
|
||||
genericDoc: GenericDocForAccompanyingPeriod,
|
||||
): Promise<WorkflowAttachment> =>
|
||||
makeFetch("POST", `/api/1.0/main/workflow/${workflowId}/attachment`, {
|
||||
relatedGenericDocKey: genericDoc.key,
|
||||
relatedGenericDocIdentifiers: genericDoc.identifiers,
|
||||
});
|
||||
|
||||
export const delete_attachment = async (
|
||||
attachment: WorkflowAttachment,
|
||||
): Promise<void> =>
|
||||
makeFetch("DELETE", `/api/1.0/main/workflow/attachment/${attachment.id}`);
|
@@ -1,3 +1,5 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
datetime8601: string;
|
||||
@@ -190,3 +192,14 @@ export interface WorkflowAvailable {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WorkflowAttachment {
|
||||
id: number;
|
||||
relatedGenericDocKey: string;
|
||||
relatedGenericDocIdentifiers: object;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
updatedAt: DateTime | null;
|
||||
updatedBy: User | null;
|
||||
genericDoc: null | GenericDoc;
|
||||
}
|
||||
|
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useTemplateRef } from "vue";
|
||||
import type { 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";
|
||||
|
||||
interface AppConfig {
|
||||
workflowId: number;
|
||||
accompanyingPeriodId: number;
|
||||
attachments: WorkflowAttachment[];
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "pickGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
(e: "removeAttachment", payload: { attachment: WorkflowAttachment }): void;
|
||||
}>();
|
||||
|
||||
type PickGenericModalType = InstanceType<typeof PickGenericDocModal>;
|
||||
|
||||
const pickDocModal = useTemplateRef<PickGenericModalType>("pickDocModal");
|
||||
const props = defineProps<AppConfig>();
|
||||
|
||||
const attachedGenericDoc = computed<GenericDocForAccompanyingPeriod[]>(
|
||||
() =>
|
||||
props.attachments
|
||||
.map((a: WorkflowAttachment) => a.genericDoc)
|
||||
.filter(
|
||||
(g: GenericDoc | null) => g !== null,
|
||||
) as GenericDocForAccompanyingPeriod[],
|
||||
);
|
||||
|
||||
const openModal = function () {
|
||||
pickDocModal.value?.openModal();
|
||||
};
|
||||
|
||||
const onPickGenericDoc = ({
|
||||
genericDoc,
|
||||
}: {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
}) => {
|
||||
emit("pickGenericDoc", { genericDoc });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pick-generic-doc-modal
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="attachedGenericDoc"
|
||||
ref="pickDocModal"
|
||||
@pickGenericDoc="onPickGenericDoc"
|
||||
></pick-generic-doc-modal>
|
||||
<attachment-list
|
||||
:attachments="props.attachments"
|
||||
@removeAttachment="(payload) => emit('removeAttachment', payload)"
|
||||
></attachment-list>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="button" class="btn btn-create" @click="openModal">
|
||||
Ajouter une pièce jointe
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface AttachmentListProps {
|
||||
attachments: WorkflowAttachment[];
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||
(e: "removeAttachment", payload: { attachment: WorkflowAttachment }): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<AttachmentListProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
v-if="props.attachments.length === 0"
|
||||
class="chill-no-data-statement text-center"
|
||||
>
|
||||
Aucune pièce jointe
|
||||
</p>
|
||||
<!-- TODO translate -->
|
||||
<div else class="flex-table">
|
||||
<div v-for="a in props.attachments" :key="a.id" class="item-bloc">
|
||||
<generic-doc-item-box
|
||||
v-if="a.genericDoc !== null"
|
||||
:generic-doc="a.genericDoc"
|
||||
></generic-doc-item-box>
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
<li v-if="a.genericDoc?.storedObject !== null">
|
||||
<document-action-buttons-group
|
||||
:stored-object="a.genericDoc.storedObject"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-delete"
|
||||
@click="emit('removeAttachment', { attachment: a })"
|
||||
></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
|
||||
interface GenericDocItemBoxProps {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
}
|
||||
|
||||
const props = defineProps<GenericDocItemBoxProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="'html' in props.genericDoc.metadata"
|
||||
v-html="props.genericDoc.metadata.html"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,281 @@
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
|
||||
interface PickGenericDocProps {
|
||||
accompanyingPeriodId: number;
|
||||
pickedList: GenericDocForAccompanyingPeriod[];
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
}
|
||||
|
||||
const props = defineProps<PickGenericDocProps>();
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "pickGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
|
||||
(
|
||||
e: "removeGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const genericDocs = ref<GenericDocForAccompanyingPeriod[]>([]);
|
||||
const loaded = ref(false);
|
||||
|
||||
const isPicked = (genericDoc: GenericDocForAccompanyingPeriod): boolean =>
|
||||
props.pickedList.findIndex(
|
||||
(element: GenericDocForAccompanyingPeriod) =>
|
||||
element.uniqueKey === genericDoc.uniqueKey,
|
||||
) !== -1;
|
||||
|
||||
onMounted(async () => {
|
||||
genericDocs.value = await fetch_generic_docs_by_accompanying_period(
|
||||
props.accompanyingPeriodId,
|
||||
);
|
||||
loaded.value = true;
|
||||
});
|
||||
|
||||
const textFilter = ref<string>("");
|
||||
const dateFromFilter = ref<string | null>(null);
|
||||
const dateToFilter = ref<string | null>(null);
|
||||
const placesFilter = ref<string[]>([]);
|
||||
|
||||
const availablePlaces = computed<string[]>(() => {
|
||||
const places = new Set<string>(
|
||||
genericDocs.value.map((genericDoc: GenericDoc) => genericDoc.key),
|
||||
);
|
||||
|
||||
return Array.from(places).sort((a, b) => (a < b ? -1 : a === b ? 0 : 1));
|
||||
});
|
||||
|
||||
const placeTrans = (str: string): string => {
|
||||
switch (str) {
|
||||
case "accompanying_course_document":
|
||||
return "Documents du parcours";
|
||||
case "person_document":
|
||||
return "Documents de l'usager";
|
||||
case "accompanying_period_calendar_document":
|
||||
return "Document des rendez-vous des parcours";
|
||||
case "accompanying_period_activity_document":
|
||||
return "Document des échanges des parcours";
|
||||
case "accompanying_period_work_evaluation_document":
|
||||
return "Document des actions d'accompagnement";
|
||||
default:
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredDocuments = computed<GenericDocForAccompanyingPeriod[]>(() => {
|
||||
if (false === loaded.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return genericDocs.value
|
||||
.filter(
|
||||
(genericDoc: GenericDocForAccompanyingPeriod) =>
|
||||
!props.toRemove
|
||||
.map((g: GenericDocForAccompanyingPeriod) => g.uniqueKey)
|
||||
.includes(genericDoc.uniqueKey),
|
||||
)
|
||||
.filter((genericDoc: GenericDocForAccompanyingPeriod) => {
|
||||
if (textFilter.value === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const needles = textFilter.value
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((str: string) => str.trim().toLowerCase())
|
||||
.filter((str: string) => str.length > 0);
|
||||
const title: string =
|
||||
"title" in genericDoc.metadata
|
||||
? (genericDoc.metadata.title as string)
|
||||
: "";
|
||||
if (title === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return needles.every((n: string) =>
|
||||
title.toLowerCase().includes(n),
|
||||
);
|
||||
})
|
||||
.filter((genericDoc: GenericDocForAccompanyingPeriod) => {
|
||||
if (placesFilter.value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return placesFilter.value.includes(genericDoc.key);
|
||||
})
|
||||
.filter((genericDoc: GenericDocForAccompanyingPeriod) => {
|
||||
if (dateToFilter.value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return genericDoc.doc_date.datetime8601 < dateToFilter.value;
|
||||
})
|
||||
.filter((genericDoc: GenericDocForAccompanyingPeriod) => {
|
||||
if (dateFromFilter.value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return genericDoc.doc_date.datetime8601 > dateFromFilter.value;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loaded">
|
||||
<div>
|
||||
<form name="f" method="get">
|
||||
<div class="accordion my-3" id="filterOrderAccordion">
|
||||
<h2 class="accordion-header" id="filterOrderHeading">
|
||||
<button
|
||||
class="accordion-button"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#filterOrderCollapse"
|
||||
aria-expanded="true"
|
||||
aria-controls="filterOrderCollapse"
|
||||
>
|
||||
<strong
|
||||
><i class="fa fa-fw fa-filter"></i>Filtrer la
|
||||
liste</strong
|
||||
>
|
||||
</button>
|
||||
</h2>
|
||||
<div
|
||||
class="accordion-collapse collapse"
|
||||
id="filterOrderCollapse"
|
||||
aria-labelledby="filterOrderHeading"
|
||||
data-bs-parent="#filterOrderAccordion"
|
||||
style=""
|
||||
>
|
||||
<div
|
||||
class="accordion-body chill_filter_order container-xxl p-5 py-2"
|
||||
>
|
||||
<div class="row my-2">
|
||||
<div class="col-sm-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="textFilter"
|
||||
type="search"
|
||||
id="f_q"
|
||||
name="f[q]"
|
||||
placeholder="Chercher dans la liste"
|
||||
class="form-control"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-misc"
|
||||
>
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-2">
|
||||
<legend
|
||||
class="col-form-label col-sm-4 required"
|
||||
>
|
||||
Date du document
|
||||
</legend>
|
||||
<div class="col-sm-8 pt-1">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Du</span>
|
||||
<input
|
||||
v-model="dateFromFilter"
|
||||
type="date"
|
||||
id="f_dateRanges_dateRange_from"
|
||||
name="f[dateRanges][dateRange][from]"
|
||||
class="form-control"
|
||||
/>
|
||||
<span class="input-group-text">Au</span>
|
||||
<input
|
||||
v-model="dateToFilter"
|
||||
type="date"
|
||||
id="f_dateRanges_dateRange_to"
|
||||
name="f[dateRanges][dateRange][to]"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-2">
|
||||
<div class="col-sm-4 col-form-label">
|
||||
Filtrer par
|
||||
</div>
|
||||
<div class="col-sm-8 pt-2">
|
||||
<div
|
||||
class="form-check"
|
||||
v-for="p in availablePlaces"
|
||||
:key="p"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="placesFilter"
|
||||
name="f[checkboxes][places][]"
|
||||
class="form-check-input"
|
||||
:value="p"
|
||||
/>
|
||||
<label class="form-check-label">{{
|
||||
placeTrans(p)
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-misc"
|
||||
>
|
||||
<i class="fa fa-fw fa-filter"></i>Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="genericDocs.length > 0" class="flex-table chill-task-list">
|
||||
<pick-generic-doc-item
|
||||
v-for="g in filteredDocuments"
|
||||
:key="g.uniqueKey"
|
||||
:accompanying-period-id="accompanyingPeriodId"
|
||||
:genericDoc="g"
|
||||
:is-picked="isPicked(g)"
|
||||
@pickGenericDoc="(payload) => emit('pickGenericDoc', payload)"
|
||||
@removeGenericDoc="
|
||||
(payload) => emit('removeGenericDoc', payload)
|
||||
"
|
||||
></pick-generic-doc-item>
|
||||
</div>
|
||||
<div v-else class="text-center chill-no-data-statement">
|
||||
Aucun document dans ce parcours
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>Chargement…</strong>
|
||||
<div
|
||||
class="spinner-border ms-auto"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import GenericDocItemBox from "ChillMainAssets/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
|
||||
interface PickGenericDocItemProps {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
accompanyingPeriodId: number;
|
||||
isPicked: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<PickGenericDocItemProps>();
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "pickGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
|
||||
(
|
||||
e: "removeGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const clickOnAddButton = () => {
|
||||
emit("pickGenericDoc", { genericDoc: props.genericDoc });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="item-bloc" :class="{ isPicked: isPicked }">
|
||||
<generic-doc-item-box
|
||||
:generic-doc="props.genericDoc"
|
||||
></generic-doc-item-box>
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
<li v-if="props.genericDoc.storedObject !== null">
|
||||
<document-action-buttons-group
|
||||
:stored-object="props.genericDoc.storedObject"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-if="!isPicked"
|
||||
type="button"
|
||||
class="btn btn-chill-green text-white"
|
||||
@click="clickOnAddButton"
|
||||
>
|
||||
<i class="bi bi-cart-plus"></i>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-chill-red text-white"
|
||||
@click="
|
||||
emit('removeGenericDoc', {
|
||||
genericDoc: props.genericDoc,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="bi bi-cart-dash"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-bloc {
|
||||
&.isPicked {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(25, 135, 84, 1) 0px,
|
||||
rgba(25, 135, 84, 0) 9px
|
||||
),
|
||||
linear-gradient(
|
||||
270deg,
|
||||
rgba(25, 135, 84, 1) 0px,
|
||||
rgba(25, 135, 84, 0) 9px
|
||||
),
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgba(25, 135, 84, 1) 0px,
|
||||
rgba(25, 135, 84, 0) 9px
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(25, 135, 84, 1) 0px,
|
||||
rgba(25, 135, 84, 0) 9px
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
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";
|
||||
|
||||
interface PickGenericDocModalProps {
|
||||
accompanyingPeriodId: number;
|
||||
toRemove: GenericDocForAccompanyingPeriod[];
|
||||
}
|
||||
|
||||
type PickGenericDocType = InstanceType<typeof PickGenericDoc>;
|
||||
|
||||
const props = defineProps<PickGenericDocModalProps>();
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||
(
|
||||
e: "pickGenericDoc",
|
||||
payload: { genericDoc: GenericDocForAccompanyingPeriod },
|
||||
): void;
|
||||
}>();
|
||||
const picker = useTemplateRef<PickGenericDocType>("picker");
|
||||
const modalOpened = ref<boolean>(false);
|
||||
const pickeds = ref<GenericDocForAccompanyingPeriod[]>([]);
|
||||
const modalClasses = { "modal-xl": true, "modal-dialog-scrollable": true };
|
||||
|
||||
const numberOfPicked = computed<number>(() => pickeds.value.length);
|
||||
|
||||
const onPicked = ({
|
||||
genericDoc,
|
||||
}: {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
}) => {
|
||||
pickeds.value.push(genericDoc);
|
||||
};
|
||||
|
||||
const onRemove = ({
|
||||
genericDoc,
|
||||
}: {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
}) => {
|
||||
const index = pickeds.value.findIndex(
|
||||
(item) => item.uniqueKey === genericDoc.uniqueKey,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error("Remove generic doc that doesn't exist");
|
||||
}
|
||||
|
||||
pickeds.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
for (let genericDoc of pickeds.value) {
|
||||
emit("pickGenericDoc", { genericDoc });
|
||||
}
|
||||
pickeds.value = [];
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
modalOpened.value = false;
|
||||
};
|
||||
|
||||
const openModal = function () {
|
||||
modalOpened.value = true;
|
||||
};
|
||||
|
||||
defineExpose({ openModal, closeModal });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
v-if="modalOpened"
|
||||
@close="closeModal"
|
||||
:modal-dialog-class="modalClasses"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<h2 class="modal-title">Ajouter une pièce jointe</h2>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<pick-generic-doc
|
||||
:accompanying-period-id="props.accompanyingPeriodId"
|
||||
:to-remove="props.toRemove"
|
||||
:picked-list="pickeds"
|
||||
ref="picker"
|
||||
@pickGenericDoc="onPicked"
|
||||
@removeGenericDoc="onRemove"
|
||||
></pick-generic-doc>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<ul v-if="numberOfPicked > 0" class="record_actions">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-chill-green text-white"
|
||||
@click="onConfirm"
|
||||
>
|
||||
<template v-if="numberOfPicked > 1">
|
||||
<i class="fa fa-plus"></i> Ajouter
|
||||
{{ numberOfPicked }} pièces jointes
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="fa fa-plus"></i> Ajouter une pièce jointe
|
||||
</template>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,109 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { _createI18n } from "../_js/i18n";
|
||||
import { WorkflowAttachment } from "ChillMainAssets/types";
|
||||
import {
|
||||
create_attachment,
|
||||
delete_attachment,
|
||||
find_attachments_by_workflow,
|
||||
} from "ChillMainAssets/lib/workflow/attachments";
|
||||
import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
import "vue-toast-notification/dist/theme-bootstrap.css";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const attachments = document.querySelectorAll<HTMLDivElement>(
|
||||
'div[data-app="workflow_attachments"]',
|
||||
);
|
||||
|
||||
attachments.forEach(async (el) => {
|
||||
const workflowId = parseInt(el.dataset.entityWorkflowId || "");
|
||||
const accompanyingPeriodId = parseInt(
|
||||
el.dataset.relatedAccompanyingPeriodId || "",
|
||||
);
|
||||
const attachments = await find_attachments_by_workflow(workflowId);
|
||||
|
||||
const app = createApp({
|
||||
template:
|
||||
'<app :workflowId="workflowId" :accompanyingPeriodId="accompanyingPeriodId" :attachments="attachments" @pickGenericDoc="onPickGenericDoc" @removeAttachment="onRemoveAttachment"></app>',
|
||||
components: { App },
|
||||
data: function () {
|
||||
return { workflowId, accompanyingPeriodId, attachments };
|
||||
},
|
||||
methods: {
|
||||
onRemoveAttachment: async function ({
|
||||
attachment,
|
||||
}: {
|
||||
attachment: WorkflowAttachment;
|
||||
}): Promise<void> {
|
||||
const index = this.$data.attachments.findIndex(
|
||||
(el: WorkflowAttachment) => el.id === attachment.id,
|
||||
);
|
||||
if (-1 === index) {
|
||||
console.warn(
|
||||
"this attachment is not associated with the workflow",
|
||||
attachment,
|
||||
);
|
||||
this.$toast.error(
|
||||
"This attachment is not associated with the workflow",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await delete_attachment(attachment);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.$toast.error("Error while removing element");
|
||||
throw error;
|
||||
}
|
||||
this.$data.attachments.splice(index, 1);
|
||||
this.$toast.success("Pièce jointe supprimée");
|
||||
},
|
||||
onPickGenericDoc: async function ({
|
||||
genericDoc,
|
||||
}: {
|
||||
genericDoc: GenericDocForAccompanyingPeriod;
|
||||
}): Promise<void> {
|
||||
console.log("picked generic doc", genericDoc);
|
||||
|
||||
// prevent to create double attachment:
|
||||
if (
|
||||
-1 !==
|
||||
this.$data.attachments.findIndex(
|
||||
(el: WorkflowAttachment) =>
|
||||
el.genericDoc?.key === genericDoc.key &&
|
||||
JSON.stringify(el.genericDoc?.identifiers) ==
|
||||
JSON.stringify(genericDoc.identifiers),
|
||||
)
|
||||
) {
|
||||
console.warn(
|
||||
"this document is already attached to the workflow",
|
||||
genericDoc,
|
||||
);
|
||||
this.$toast.error(
|
||||
"Ce document est déjà attaché au workflow",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const attachment = await create_attachment(
|
||||
workflowId,
|
||||
genericDoc,
|
||||
);
|
||||
this.$data.attachments.push(attachment);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const i18n = _createI18n({});
|
||||
app.use(i18n);
|
||||
app.use(ToastPlugin);
|
||||
|
||||
app.mount(el);
|
||||
});
|
||||
});
|
@@ -1,123 +1,5 @@
|
||||
{# TODO
|
||||
Check if this template is used
|
||||
Adapt condition or Delete it
|
||||
#}
|
||||
{% if random(1) == 0 %}
|
||||
|
||||
{# For a document #}
|
||||
<h2>{{ 'Document'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-4x fa-file-text-o text-success"></i>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h3>Imprimé unique, parcours n°14635</h3>
|
||||
<small>Document PDF (6.2 Mo)</small>
|
||||
<p class="mt-2">
|
||||
Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{# For an action #}
|
||||
<h2>{{ 'Accompanying Course Action'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="flex-table accompanying-course-work">
|
||||
{# dynamic insertion
|
||||
::: TODO delete all static insertion, remove condition and pass work object in inclusion
|
||||
#}{% if dynamic is defined %}
|
||||
|
||||
{% set work = '<pass work object here>' %}
|
||||
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{# BEGIN static insertion #}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">Exercer un AEB > Conclure l'AEB
|
||||
<ul class="small_in_title columns mt-1">
|
||||
<li><span class="item-key">Date de début : </span><b>25/11/2021</b></li>
|
||||
<li><span class="item-key">Date de fin : </span><b>10/03/2022</b></li>
|
||||
</ul>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Référent</h3></div>
|
||||
<div class="wl-col list"><p class="wl-item">Fred</p></div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Usagers du parcours</h3></div>
|
||||
<div class="wl-col list"><span class="wl-item">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="1937" data-action="show" data-button-text="Vernon SUBUTEX" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Vernon SUBUTEX</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
<span class="wl-item"><span class="onthefly-container" data-target-name="person" data-target-id="1941" data-action="show" data-button-text="Juan RAMON" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Juan RAMON</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Problématique sociale</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item social-issues">
|
||||
<span class="chill-entity entity-social-issue"><span class="badge bg-chill-l-gray text-dark"><span class="parent-0">AD - PREVENTION, ACCES AUX DROITS, BUDGET ></span><span class="child">SOUTIEN EQUILIBRE BUDGET</span></span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row column">
|
||||
<table class="obj-res-eval smallfont my-3">
|
||||
<thead>
|
||||
<tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
|
||||
<th class="res"><h4 class="title_label">Résultats - orientations</h4></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="obj">
|
||||
<p class="chill-no-data-statement">Aucun objectif - motif - dispositif</p>
|
||||
</td>
|
||||
<td class="res">
|
||||
<ul class="result_list">
|
||||
<li>Résultat : Arrêt à l'initiative du ménage pour déménagement</li>
|
||||
<li>Orientation vers une MASP</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="updatedBy">
|
||||
Dernière mise à jour par
|
||||
<b><span class="chill-entity entity-user">Fred<span class="user-job">(Responsable tous les territoires)</span><span class="main-scope">(ASE)</span></span></b>,<br>
|
||||
le 3 décembre 2021 à 15:19
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# END static insertion #}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if related_accompanying_period is not null %}
|
||||
<h2>{{ 'workflow.attachments.title'|trans }}</h2>
|
||||
|
||||
<div data-app="workflow_attachments" data-workflow-id="{{ entity_workflow.id }}" data-related-accompanying-period-id="{{ related_accompanying_period.id }}" data-entity-workflow-id="{{ entity_workflow.id }}" ></div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="button" class="btn btn-misc">
|
||||
<i class="fa fa-download fa-fw"></i>{{ 'Download'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
{% set x = random(1) %}
|
||||
<button class="btn btn-update change-icon {% if x == 1 %}disabled{% endif %}">
|
||||
<i class="fa fa-fw fa-{% if x == 0 %}un{% endif %}lock"></i>
|
||||
{{ 'Edit'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -11,6 +11,7 @@
|
||||
{{ encore_entry_script_tags('page_workflow_show') }}
|
||||
{{ encore_entry_script_tags('mod_wopi_link') }}
|
||||
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
|
||||
{{ encore_entry_script_tags('mod_workflow_attachment') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
@@ -20,6 +21,7 @@
|
||||
{{ encore_entry_link_tags('page_workflow_show') }}
|
||||
{{ encore_entry_link_tags('mod_wopi_link') }}
|
||||
{{ encore_entry_link_tags('mod_document_action_buttons_group') }}
|
||||
{{ encore_entry_link_tags('mod_workflow_attachment') }}
|
||||
{% endblock %}
|
||||
|
||||
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %}
|
||||
@@ -58,6 +60,8 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?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\MainBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\GenericDoc\Manager;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\GenericDocNormalizer;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class EntityWorkflowAttachmentNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public const LINKED = 'entity_workflow_attachment_linked';
|
||||
|
||||
public function __construct(
|
||||
private readonly Manager $manager,
|
||||
) {}
|
||||
|
||||
public function normalize($object, $format = null, array $context = []): array
|
||||
{
|
||||
/** @var EntityWorkflowAttachment $object */
|
||||
$genericDoc = $this->manager->buildOneGenericDoc($object->getRelatedGenericDocKey(), $object->getRelatedGenericDocIdentifiers());
|
||||
|
||||
return [
|
||||
'id' => $object->getId(),
|
||||
'relatedGenericDocKey' => $object->getRelatedGenericDocKey(),
|
||||
'relatedGenericDocIdentifiers' => $object->getRelatedGenericDocIdentifiers(),
|
||||
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||
'updatedAt' => $this->normalizer->normalize($object->getUpdatedAt(), $format, $context),
|
||||
'updatedBy' => $this->normalizer->normalize($object->getUpdatedBy(), $format, $context),
|
||||
'genericDoc' => $this->normalizer->normalize($genericDoc, $format, [
|
||||
GenericDocNormalizer::ATTACHED_STORED_OBJECT_PROXY => $object->getProxyStoredObject(), ...$context,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null)
|
||||
{
|
||||
return 'json' === $format && $data instanceof EntityWorkflowAttachment;
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
|
||||
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -189,6 +190,11 @@ class WorkflowViewSendPublicControllerTest extends TestCase
|
||||
throw new \BadMethodCallException('not implemented');
|
||||
}
|
||||
|
||||
public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod
|
||||
{
|
||||
throw new \BadMethodCallException('not implemented');
|
||||
}
|
||||
|
||||
public function getRelatedObjects(object $object): array
|
||||
{
|
||||
throw new \BadMethodCallException('not implemented');
|
||||
|
@@ -0,0 +1,66 @@
|
||||
<?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\MainBundle\Tests\Workflow\Attachment;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\GenericDoc\GenericDocDTO;
|
||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentAction;
|
||||
use Chill\MainBundle\Workflow\Attachment\AddAttachmentRequestDTO;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AddAttachmentActionTest extends TestCase
|
||||
{
|
||||
private EntityManagerInterface $entityManagerMock;
|
||||
private AddAttachmentAction $addAttachmentAction;
|
||||
|
||||
private ManagerInterface $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManagerMock = $this->createMock(EntityManagerInterface::class);
|
||||
$this->manager = $this->createMock(ManagerInterface::class);
|
||||
$this->addAttachmentAction = new AddAttachmentAction($this->entityManagerMock, $this->manager);
|
||||
}
|
||||
|
||||
public function testInvokeCreatesAndPersistsEntityWorkflowAttachment(): void
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new AddAttachmentRequestDTO($entityWorkflow);
|
||||
$dto->relatedGenericDocKey = 'doc_key';
|
||||
$dto->relatedGenericDocIdentifiers = ['id' => 1];
|
||||
$this->manager->method('buildOneGenericDoc')->willReturn($g = new GenericDocDTO('doc_key', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod()));
|
||||
$this->manager->method('fetchStoredObject')->with($g)->willReturn($storedObject = new StoredObject());
|
||||
|
||||
$this->entityManagerMock
|
||||
->expects($this->once())
|
||||
->method('persist')
|
||||
->with($this->isInstanceOf(EntityWorkflowAttachment::class));
|
||||
|
||||
$result = $this->addAttachmentAction->__invoke($dto);
|
||||
|
||||
$this->assertInstanceOf(EntityWorkflowAttachment::class, $result);
|
||||
$this->assertSame('doc_key', $result->getRelatedGenericDocKey());
|
||||
$this->assertSame(['id' => 1], $result->getRelatedGenericDocIdentifiers());
|
||||
$this->assertSame($entityWorkflow, $result->getEntityWorkflow());
|
||||
$this->assertSame($storedObject, $result->getProxyStoredObject());
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
<?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\MainBundle\Workflow\Attachment;
|
||||
|
||||
use Chill\DocStoreBundle\GenericDoc\Exception\AssociatedStoredObjectNotFound;
|
||||
use Chill\DocStoreBundle\GenericDoc\ManagerInterface;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class AddAttachmentAction
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $em, private readonly ManagerInterface $manager) {}
|
||||
|
||||
/**
|
||||
* @throws AssociatedStoredObjectNotFound
|
||||
*/
|
||||
public function __invoke(AddAttachmentRequestDTO $dto): EntityWorkflowAttachment
|
||||
{
|
||||
$genericDoc = $this->manager->buildOneGenericDoc($dto->relatedGenericDocKey, $dto->relatedGenericDocIdentifiers);
|
||||
|
||||
if (null === $genericDoc) {
|
||||
throw new \RuntimeException(sprintf('could not build any generic doc, %s key and %s identifiers', $dto->relatedGenericDocKey, json_encode($dto->relatedGenericDocIdentifiers)));
|
||||
}
|
||||
|
||||
$storedObject = $this->manager->fetchStoredObject($genericDoc);
|
||||
|
||||
$attachement = new EntityWorkflowAttachment($dto->relatedGenericDocKey, $dto->relatedGenericDocIdentifiers, $dto->entityWorkflow, $storedObject);
|
||||
|
||||
$this->em->persist($attachement);
|
||||
|
||||
return $attachement;
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?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\MainBundle\Workflow\Attachment;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final class AddAttachmentRequestDTO
|
||||
{
|
||||
#[Serializer\Groups('write')]
|
||||
#[Assert\NotNull()]
|
||||
public ?string $relatedGenericDocKey = null;
|
||||
#[Serializer\Groups('write')]
|
||||
#[Assert\NotNull()]
|
||||
public ?array $relatedGenericDocIdentifiers = null;
|
||||
|
||||
public function __construct(
|
||||
public readonly EntityWorkflow $entityWorkflow,
|
||||
) {}
|
||||
}
|
@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
|
||||
@@ -35,6 +36,11 @@ interface EntityWorkflowHandlerInterface
|
||||
*/
|
||||
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
|
||||
|
||||
/**
|
||||
* Return a related accompanying period, if the workflow can be related in an accompanying period.
|
||||
*/
|
||||
public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod;
|
||||
|
||||
public function getRelatedObjects(object $object): array;
|
||||
|
||||
/**
|
||||
|
@@ -18,6 +18,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
@@ -157,4 +158,9 @@ class EntityWorkflowManager
|
||||
{
|
||||
return $this->getHandler($entityWorkflow)->getSuggestedThirdParties($entityWorkflow);
|
||||
}
|
||||
|
||||
public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod
|
||||
{
|
||||
return $this->getHandler($entityWorkflow)->getRelatedAccompanyingPeriod($entityWorkflow);
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
@@ -146,12 +147,14 @@ class WorkflowRelatedEntityPermissionHelper
|
||||
{
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
if (!$currentUser instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
// so, the workflow is running... We return true if the current user is involved
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
if ($step->getAllDestUser()->contains($currentUser)) {
|
||||
return true;
|
||||
}
|
||||
if ($entityWorkflow->isUserInvolved($currentUser)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,50 @@ servers:
|
||||
|
||||
components:
|
||||
schemas:
|
||||
EntityWorkflowAttachment:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: u64
|
||||
minimum: 0
|
||||
relatedGenericDocKey:
|
||||
type: string
|
||||
relatedGenericDocIdentifiers:
|
||||
type: object
|
||||
AddAttachmentRequest:
|
||||
description: "A request to add attachment in an entity workflow"
|
||||
type: object
|
||||
properties:
|
||||
relatedGenericDocKey:
|
||||
type: string
|
||||
relatedGenericDocIdentifiers:
|
||||
type: object
|
||||
PaginatedResult:
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
type: number
|
||||
format: u64
|
||||
pagination:
|
||||
type: object
|
||||
properties:
|
||||
first:
|
||||
type: number
|
||||
format: u64
|
||||
minimum: 0
|
||||
items_per_page:
|
||||
type: number
|
||||
format: u64
|
||||
minimum: 0
|
||||
next:
|
||||
type: string
|
||||
nullable: true
|
||||
previous:
|
||||
type: string
|
||||
nullable: true
|
||||
more:
|
||||
type: boolean
|
||||
Date:
|
||||
type: object
|
||||
properties:
|
||||
@@ -990,3 +1034,70 @@ paths:
|
||||
$ref: '#/components/schemas/UserGroup'
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
/1.0/main/workflow/{id}/attachment:
|
||||
get:
|
||||
tags:
|
||||
- workflow
|
||||
summary: Get a list of attachements for a given workflow
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The entity workflow id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
200:
|
||||
description: "ok"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EntityWorkflowAttachment'
|
||||
post:
|
||||
tags:
|
||||
- workflow
|
||||
summary: Create a new attachment
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The entity workflow id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddAttachmentRequest'
|
||||
responses:
|
||||
200:
|
||||
description: "ok"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityWorkflowAttachment'
|
||||
/1.0/main/workflow/attachment/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- workflow
|
||||
summary: Remove an attachment
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The entity workflow 's attachment id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
204:
|
||||
description: "resource was deleted successfully"
|
||||
|
||||
|
@@ -150,6 +150,10 @@ module.exports = function (encore, entries) {
|
||||
"mod_news",
|
||||
__dirname + "/Resources/public/module/news/index.js",
|
||||
);
|
||||
encore.addEntry(
|
||||
"mod_workflow_attachment",
|
||||
__dirname + "/Resources/public/vuejs/WorkflowAttachment/index",
|
||||
);
|
||||
|
||||
// Vue entrypoints
|
||||
encore.addEntry(
|
||||
|
@@ -33,6 +33,8 @@ services:
|
||||
# workflow related
|
||||
Chill\MainBundle\Workflow\:
|
||||
resource: '../Workflow/'
|
||||
exclude:
|
||||
- '../Workflow/Attachment/AddAttachmentRequestDTO.php'
|
||||
|
||||
Chill\MainBundle\Workflow\EntityWorkflowManager:
|
||||
arguments:
|
||||
|
@@ -0,0 +1,51 @@
|
||||
<?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\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20241129112740 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add table for EntityWorkflowAttachment';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_attachment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_main_workflow_entity_attachment (id INT NOT NULL, relatedGenericDocKey VARCHAR(255) NOT NULL, relatedGenericDocIdentifiers JSONB NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, entityWorkflow_id INT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_279415FFFB054143 ON chill_main_workflow_entity_attachment (entityWorkflow_id)');
|
||||
$this->addSql('CREATE INDEX IDX_279415FF3174800F ON chill_main_workflow_entity_attachment (createdBy_id)');
|
||||
$this->addSql('CREATE INDEX IDX_279415FF65FF1AEC ON chill_main_workflow_entity_attachment (updatedBy_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX unique_generic_doc_by_workflow ON chill_main_workflow_entity_attachment (relatedGenericDocKey, relatedGenericDocIdentifiers, entityworkflow_id)');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_attachment.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_attachment.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FFFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FF3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FF65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD storedobject_id INT NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FFEE684399 FOREIGN KEY (storedobject_id) REFERENCES chill_doc.stored_object (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_279415FFEE684399 ON chill_main_workflow_entity_attachment (storedobject_id)');
|
||||
$this->addSql('ALTER INDEX idx_279415fffb054143 RENAME TO IDX_279415FF7D99CE94');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_attachment_id_seq CASCADE');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FFFB054143');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FF3174800F');
|
||||
$this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FF65FF1AEC');
|
||||
$this->addSql('DROP TABLE chill_main_workflow_entity_attachment');
|
||||
}
|
||||
}
|
@@ -612,6 +612,9 @@ workflow:
|
||||
reject_signature_of: Rejet de la signature de %signer%
|
||||
reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer%
|
||||
|
||||
attachments:
|
||||
title: Pièces jointes
|
||||
|
||||
Subscribe final: Recevoir une notification à l'étape finale
|
||||
Subscribe all steps: Recevoir une notification à chaque étape
|
||||
CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows
|
||||
|
Reference in New Issue
Block a user