Merge branch 'homepage/add-workflow' into 'master'

add workflow on widget homepage

See merge request Chill-Projet/chill-bundles!343
This commit is contained in:
Julien Fastré 2022-02-17 22:19:25 +00:00
commit 9565464489
12 changed files with 308 additions and 21 deletions

View File

@ -14,19 +14,46 @@ namespace Chill\DocStoreBundle\Workflow;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
{
private EntityRepository $repository;
private TranslatorInterface $translator;
/**
* TODO: injecter le repository directement.
*/
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, TranslatorInterface $translator)
{
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
$this->translator = $translator;
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$course = $this->getRelatedEntity($entityWorkflow)
->getCourse();
$persons = [];
if (null !== $course) {
$persons = $course->getCurrentParticipations()->map(static function (AccompanyingPeriodParticipation $participation) {
return $participation->getPerson();
})->toArray();
}
return [
'persons' => array_values($persons),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
return $this->translator->trans('workflow.Document (n°%doc%)', ['%doc%' => $entityWorkflow->getRelatedEntityId()]);
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingCourseDocument

View File

@ -11,7 +11,12 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -21,17 +26,71 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
class WorkflowApiController
{
private EntityManagerInterface $entityManager;
private EntityWorkflowRepository $entityWorkflowRepository;
private PaginatorFactory $paginatorFactory;
private Security $security;
public function __construct(Security $security, EntityManagerInterface $entityManager)
{
private SerializerInterface $serializer;
public function __construct(
EntityManagerInterface $entityManager,
EntityWorkflowRepository $entityWorkflowRepository,
PaginatorFactory $paginatorFactory,
Security $security,
SerializerInterface $serializer
) {
$this->entityManager = $entityManager;
$this->entityWorkflowRepository = $entityWorkflowRepository;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security;
$this->serializer = $serializer;
}
/**
* Return a list of workflow which are waiting an action for the user.
*
* @Route("/api/1.0/main/workflow/my", methods={"GET"})
*/
public function myWorkflow(Request $request): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) {
throw new AccessDeniedException();
}
$total = $this->entityWorkflowRepository->countByDest($this->security->getUser());
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByDest(
$this->security->getUser(),
['id' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new JsonResponse(
$this->serializer->serialize(new Collection($workflows, $paginator), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**

View File

@ -1,7 +1,7 @@
<template>
<h2>{{ $t('main_title') }}</h2>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link"
@ -23,7 +23,6 @@
:class="{'active': activeTab === 'MyAccompanyingCourses'}"
@click="selectTab('MyAccompanyingCourses')">
{{ $t('my_accompanying_courses.tab') }}
<tab-counter :count="state.accompanyingCourses.count"></tab-counter>
</a>
</li>
<li class="nav-item">
@ -51,11 +50,19 @@
<tab-counter :count="state.tasks.alert.count"></tab-counter>
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorkflows'}"
@click="selectTab('MyWorkflows')">
{{ $t('my_workflows.tab') }}
<tab-counter :count="state.workflows.count"></tab-counter>
</a>
</li>
<li class="nav-item loading ms-auto py-2" v-if="loading">
<i class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray" :title="$t('loading')"></i>
</li>
</ul>
<div class="my-4">
<my-customs
v-if="activeTab === 'MyCustoms'">
@ -75,6 +82,9 @@
<my-notifications
v-else-if="activeTab === 'MyNotifications'">
</my-notifications>
<my-workflows
v-else-if="activeTab === 'MyWorkflows'">
</my-workflows>
</div>
</template>
@ -85,6 +95,7 @@ import MyEvaluations from './MyEvaluations';
import MyTasks from './MyTasks';
import MyAccompanyingCourses from './MyAccompanyingCourses';
import MyNotifications from './MyNotifications';
import MyWorkflows from './MyWorkflows.vue';
import TabCounter from './TabCounter';
import { mapState } from "vuex";
@ -95,6 +106,7 @@ export default {
MyWorks,
MyEvaluations,
MyTasks,
MyWorkflows,
MyAccompanyingCourses,
MyNotifications,
TabCounter,
@ -126,6 +138,7 @@ export default {
'MyWorks',
'MyEvaluations',
'MyTasks',
'MyWorkflows',
]) {
this.$store.dispatch('getByTab', { tab: m, param: "countOnly=1" });
}

View File

@ -66,6 +66,8 @@ export default {
return appMessages.fr.the_activity;
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
return appMessages.fr.the_course;
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
return appMessages.fr.the_workflow;
default:
throw 'notification type unknown';
}
@ -76,6 +78,8 @@ export default {
return `/fr/activity/${n.relatedEntityId}/show`
case 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod':
return `/fr/parcours/${n.relatedEntityId}`
case 'Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow':
return `/fr/main/workflow/${n.relatedEntityId}/show`
default:
throw 'notification type unknown';
}

View File

@ -0,0 +1,88 @@
<template>
<div class="alert alert-light">{{ $t('my_workflows.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Object_workflow') }}</th>
<th scope="col">{{ $t('Step') }}</th>
<th scope="col">{{ $t('concerned_users') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
<td>{{ w.title }}</td>
<td>
<div class="workflow">
<div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
<span class="mx-2">{{ getStep(w) }}</span>
</div>
</div>
</td>
<td v-if="w.datas.persons !== null">
<span v-for="p in w.datas.persons" class="me-1" :key="p.id">
<on-the-fly
:type="p.type"
:id="p.id"
:buttonText="p.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_workflow') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly
},
computed: {
...mapState([
'workflows',
]),
...mapGetters([
'isWorkflowsLoaded',
]),
noResults() {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return this.workflows.count === 0;
}
},
},
methods: {
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1
return w.steps[lastStep].currentStep.text;
}
},
}
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>

View File

@ -22,6 +22,10 @@ const appMessages = {
tab: "Mes notifications",
description: "Liste des notifications reçues et non lues.",
},
my_workflows: {
tab: "Mes workflows",
description: "Liste des workflows en attente d'une action."
},
opening_date: "Date d'ouverture",
social_issues: "Problématiques sociales",
concerned_persons: "Usagers concernés",
@ -33,12 +37,16 @@ const appMessages = {
From: "Expéditeur",
Subject: "Objet",
Entity: "Associé à",
Step: "Étape",
concerned_users: "Usagers concernés",
Object_workflow: "Objet du workflow",
show_entity: "Voir {entity}",
the_activity: "l'échange",
the_course: "le parcours",
the_action: "l'action",
the_evaluation: "l'évaluation",
the_task: "la tâche",
the_workflow: "le workflow",
StartDate: "Date d'ouverture",
SocialAction: "Action d'accompagnement",
no_data: "Aucun résultats",
@ -61,4 +69,4 @@ Object.assign(appMessages.fr);
export {
appMessages
};
};

View File

@ -1,12 +1,6 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import MyCustoms from "../MyCustoms";
import MyWorks from "../MyWorks";
import MyEvaluations from "../MyEvaluations";
import MyTasks from "../MyTasks";
import MyAccompanyingCourses from "../MyAccompanyingCourses";
import MyNotifications from "../MyNotifications";
const debug = process.env.NODE_ENV !== 'production';
@ -27,6 +21,7 @@ const store = createStore({
},
accompanyingCourses: {},
notifications: {},
workflows: {},
errorMsg: [],
loading: false
},
@ -49,6 +44,9 @@ const store = createStore({
isNotificationsLoaded(state) {
return !isEmpty(state.notifications);
},
isWorkflowsLoaded(state) {
return !isEmpty(state.workflows);
},
counter(state) {
return {
works: state.works.count,
@ -57,6 +55,7 @@ const store = createStore({
tasksAlert: state.tasks.alert.count,
accompanyingCourses: state.accompanyingCourses.count,
notifications: state.notifications.count,
workflows: state.workflows.count
}
}
},
@ -85,6 +84,9 @@ const store = createStore({
//console.log('addNotifications', notifications);
state.notifications = notifications;
},
addWorkflows(state, workflows) {
state.workflows = workflows;
},
setLoading(state, bool) {
state.loading = bool;
},
@ -180,14 +182,30 @@ const store = createStore({
const url = `/api/1.0/main/notification/my/unread${'?'+ param}`;
makeFetch('GET', url)
.then((response) => {
console.log('notifications', response)
commit('addNotifications', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
;
});
}
break;
case 'MyWorkflows':
if (!getters.isWorflowsLoaded) {
commit('setLoading', true);
const url = '/api/1.0/main/workflow/my';
makeFetch('GET', url)
.then((response) => {
console.log('workflows', response)
commit('addWorkflows', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
});
}
break;
default:

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@ -22,12 +23,18 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
{
use NormalizerAwareTrait;
private EntityWorkflowManager $entityWorkflowManager;
private MetadataExtractor $metadataExtractor;
private Registry $registry;
public function __construct(MetadataExtractor $metadataExtractor, Registry $registry)
{
public function __construct(
EntityWorkflowManager $entityWorkflowManager,
MetadataExtractor $metadataExtractor,
Registry $registry
) {
$this->entityWorkflowManager = $entityWorkflowManager;
$this->metadataExtractor = $metadataExtractor;
$this->registry = $registry;
}
@ -40,6 +47,7 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
public function normalize($object, ?string $format = null, array $context = [])
{
$workflow = $this->registry->get($object, $object->getWorkflowName());
$handler = $this->entityWorkflowManager->getHandler($object);
return [
'type' => 'entity_workflow',
@ -49,6 +57,8 @@ class EntityWorkflowNormalizer implements NormalizerInterface, NormalizerAwareIn
'workflow' => $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow),
'currentStep' => $this->normalizer->normalize($object->getCurrentStep(), $format, $context),
'steps' => $this->normalizer->normalize($object->getStepsChained(), $format, $context),
'datas' => $this->normalizer->normalize($handler->getEntityData($object), $format, $context),
'title' => $handler->getEntityTitle($object),
];
}

View File

@ -15,6 +15,10 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
interface EntityWorkflowHandlerInterface
{
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array;
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
/**

View File

@ -125,6 +125,11 @@ components:
type: object
type:
type: string
Workflow:
type: object
properties:
id:
type: integer
paths:
/1.0/search.json:
@ -795,4 +800,20 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UserJob'
/1.0/main/workflow/my:
get:
tags:
- workflow
summary: Return a list of workflows awaiting for user's action
responses:
200:
description: "ok"
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Workflow'
403:
description: "Unauthorized"

View File

@ -16,14 +16,32 @@ use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface
{
private AccompanyingPeriodWorkEvaluationRepository $repository;
public function __construct(AccompanyingPeriodWorkEvaluationRepository $repository)
private TranslatorInterface $translator;
public function __construct(AccompanyingPeriodWorkEvaluationRepository $repository, TranslatorInterface $translator)
{
$this->repository = $repository;
$this->translator = $translator;
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$evaluation = $this->getRelatedEntity($entityWorkflow);
return [
'persons' => $evaluation->getAccompanyingPeriodWork()->getPersons(),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
return $this->translator->trans('workflow.Evaluation (n°%eval%)', ['%eval%' => $entityWorkflow->getRelatedEntityId()]);
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWorkEvaluation

View File

@ -15,17 +15,34 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface
{
private AccompanyingPeriodWorkRepository $repository;
public function __construct(AccompanyingPeriodWorkRepository $repository)
private TranslatorInterface $translator;
public function __construct(AccompanyingPeriodWorkRepository $repository, TranslatorInterface $translator)
{
$this->repository = $repository;
$this->translator = $translator;
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
return [
'persons' => $this->getRelatedEntity($entityWorkflow)
->getPersons(),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
return $this->translator->trans('workflow.Work (n°%w%)', ['%w%' => $entityWorkflow->getRelatedEntityId()]);
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWork
{
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}