Compare commits

...

15 Commits

Author SHA1 Message Date
2fc6e18d0f merge master 2023-10-26 09:36:14 +02:00
Lucas Silva
80ea4bbdd2 dump 2023-05-16 10:33:01 +02:00
Lucas Silva
801c693ef9 dump 2023-05-16 10:30:22 +02:00
Lucas Silva
c1c9562e67 finalisation présentation 2023-05-16 10:28:22 +02:00
Lucas Silva
b9e580af9a First Widget Bar + Number of notification unread. All is hardcoded 2023-05-16 02:17:36 +02:00
Lucas Silva
790c7f6724 First Widget Bar + Number of notification unread. All is hardcoded 2023-05-16 02:16:49 +02:00
Lucas Silva
1be91bb392 widgetManager 2023-05-09 12:58:02 +02:00
Lucas Silva
93f39ebe5b vue hardcode from controller 2023-05-09 12:57:06 +02:00
Lucas Silva
176c3c0e27 adding some comments 2023-05-05 10:42:40 +02:00
Lucas Silva
59cd8466be Adding the file for visitor pattern + function that need to be implemented. 2023-05-05 10:38:19 +02:00
Lucas Silva
ef9e872394 adding hardcode config first 2023-05-05 10:34:49 +02:00
Lucas Silva
51a46ab5d7 fix vue component 2023-05-05 10:34:32 +02:00
Lucas Silva
191b416c6c adding vue component 2023-05-05 10:23:06 +02:00
6028efdc7c Merge branch 'master' into 68-feature-dashboard-user 2023-04-25 19:55:32 +02:00
Lucas Silva
60c9e037a6 Addind API endpoint for savedExport 2023-04-25 13:43:55 +02:00
17 changed files with 717 additions and 31 deletions

View File

@@ -31,7 +31,9 @@
"typescript": "^4.7.2",
"vue-loader": "^17.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
"webpack-cli": "^5.0.1",
"chart.js": "^4.2.1",
"vue-chartjs": "^5.2.0"
},
"dependencies": {
"@fullcalendar/core": "^6.1.4",

View File

@@ -32,6 +32,7 @@ use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Chill\MainBundle\Service\EntityInfo\ViewEntityInfoProviderInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Chill\MainBundle\Widget\WidgetHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -62,9 +63,10 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.workflow_handler');
$container->registerForAutoconfiguration(CronJobInterface::class)
->addTag('chill_main.cron_job');
$container->registerForAutoconfiguration(WidgetHandlerInterface::class)
->addTag('chill_main.widget_handler');
$container->registerForAutoconfiguration(ViewEntityInfoProviderInterface::class)
->addTag('chill_main.entity_info_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);

View File

@@ -0,0 +1,133 @@
<?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\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Widget\WidgetHandlerManager;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security;
use PHPUnit\Util\Json;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @Route("/{_locale}/dashboard")
*/
class DashboardHomepageController extends AbstractController{
private WidgetHandlerManager $widgetHandlerManager;
private Security $security;
private UserRepository $userRepository;
private EntityManagerInterface $em;
private LoggerInterface $logger;
private SerializerInterface $serializer;
private PaginatorFactory $paginatorFactory;
public function __construct(
//AccompanyingPeriodRepository $accompanyingPeriodRepository,
WidgetHandlerManager $widgetHandlerManager,
Security $security,
UserRepository $userRepository,
EntityManagerInterface $em,
LoggerInterface $logger,
SerializerInterface $serializer,
PaginatorFactory $paginatorFactory,
)
{
//$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->widgetHandlerManager = $widgetHandlerManager;
$this->security = $security;
$this->userRepository = $userRepository;
$this->em = $em;
$this->logger = $logger;
$this->serializer = $serializer;
$this->paginatorFactory = $paginatorFactory;
}
/**
* @Route("/raw_data", name="chill_main_widget_raw_data")
* @return JsonResponse
*/
public function index(): JsonResponse
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
if (!$this->security->getUser() instanceof User) {
throw new AccessDeniedHttpException('You must be authenticated and a user to see the dashboard');
}
// --------------Bar---------------------
$config = [
'alias' => 'bar',
];
$context = [
//'user' => $this->security->getUser(),
//Hardcoder pour resultat
'user' => 19,
'what' => 'accompanying_period_by_month',
];
// --------------Number------------------
/*$config = [
'alias' => 'number',
];
$context = [
'user' => $this->security->getUser(),
//'what' => 'notification_unread',
'what' => 'notification_sender',
];*/
$data = $this->widgetHandlerManager->getDataForWidget($config, $context);
return new JsonResponse($data,Response::HTTP_OK,[]);
}
/**
* @Route("/widget/{context}", name="chill_main_widget_get_data")
*/
public function getDataForWidget(Request $request, string $context): JsonResponse
{
//Retrieve data from request in vue component
$requestData = json_decode($request->getContent(), true);
dump($requestData);
// Process the widget data using the WidgetHandlerManager
//$handler = $this->widgetHandlerManager->getHandler($requestData);
//dump($handler);
// Return the widget data in JSON response
//return new JsonResponse($this->getData($requestData));
return new JsonResponse($requestData);
}
}

View File

@@ -0,0 +1,18 @@
<?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\CRUD\Controller\ApiController;
class SavedExportApiController extends ApiController
{
}

View File

@@ -20,6 +20,7 @@ use Chill\MainBundle\Controller\LanguageController;
use Chill\MainBundle\Controller\LocationController;
use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\SavedExportApiController;
use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController;
@@ -54,6 +55,7 @@ use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\CenterType;
@@ -767,7 +769,28 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => SavedExport::class,
'controller' => SavedExportApiController::class,
'name' => 'saved_export',
'base_path' => '/api/1.0/main/saved-export',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
]
]
]);
}
}

View File

@@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Entity
@@ -35,6 +36,7 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*
* @Assert\NotBlank
* @Groups({"read"})
*/
private string $description = '';
@@ -49,11 +51,13 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Column(name="id", type="uuid", unique="true")
*
* @ORM\GeneratedValue(strategy="NONE")
* @Groups({"read"})
*/
private UuidInterface $id;
/**
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
* @Groups({"read"})
*/
private array $options = [];
@@ -61,11 +65,13 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*
* @Assert\NotBlank
* @Groups({"read"})
*/
private string $title = '';
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @Groups({"read"})
*/
private User $user;

View File

@@ -38,39 +38,33 @@
</ul>
</div>
</div>
<!--
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom2">
Mon dashboard personnalisé
<MyWidget/>
</div>
</div>
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom3">
Mon dashboard personnalisé
</div>
</div>
<div class="mbloc col col-sm-6 col-lg-4">
<div class="custom4">
Mon dashboard personnalisé
</div>
</div>
-->
</div>
</template>
<script>
import MyWidget from './MyWidget.vue'
import { mapGetters } from "vuex";
import Masonry from 'masonry-layout/masonry';
export default {
name: "MyCustoms",
components:{
MyWidget
},
data() {
return {
counterClass: {
counter: true //hack to pass class 'counter' in i18n-t
}
},
}
},
computed: {
@@ -82,8 +76,8 @@ export default {
mounted() {
const elem = document.querySelector('#dashboards');
const masonry = new Masonry(elem, {});
}
}
},
};
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,91 @@
<template>
<div class="container">
<Bar v-if="loaded_bar" :data="chartData" :options="chartOptions"/>
<span v-if="loaded_number">{{ number }}</span>
</div>
</template>
<script lang="ts">
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale
} from 'chart.js'
import {Bar} from 'vue-chartjs'
import {defineComponent} from 'vue'
import {makeFetch} from "../../lib/api/apiMethods";
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
export default defineComponent({
name: 'MyWidget',
components: {
Bar,
},
data() {
return {
loaded_bar: false,
loaded_number: false,
chartData: null,
chartOptions: null,
number: "",
}
},
async mounted() {
this.loaded_bar = false;
this.loaded_number = false;
const url = '/fr/dashboard/raw_data';
try {
const response: { data: any, options: any, type: any } = await makeFetch("GET", url);
this.chartData = response.data;
this.chartOptions = response.options;
if (response.type == 'bar') {
this.loaded_bar = true;
} else {
this.loaded_number = true
this.number = response.data.datasets[0].data[0] + " "+ response.data.labels[0];
}
} catch (error) {
console.log(error);
}
},
/*methods: {
async makeNumberWidget() {
let body = {
config: {
alias: 'number'
},
context: {
what: 'notification_unread',
user: 19 //Ne rien mettre ou via les refs
}
};
try {
const response: {
data: any,
options: any,
type: any
} = await makeFetch('POST', '/{_locale}//{_locale}/dashboard/widget/number', body)
this.chartData = response.data;
this.chartOptions = response.options;
this.loaded_number = true;
this.number = response.data.datasets[0].data[0] + response.data.labels[0];
} catch (error) {
console.log(error)
}
},
}*/
})
</script>
<style lang="scss" scoped>
span {
font-size: x-large;
color: #0a53be;
}
</style>

View File

@@ -0,0 +1,28 @@
export const data = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
datasets: [
{
label: 'Data One',
backgroundColor: '#f87979',
data: [40, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11]
}
]
}
export const options = {
responsive: true,
maintainAspectRatio: false
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Widget;
interface WidgetHandlerInterface
{
/**
* Return true if the handler supports the handling for this widget.
*/
public function supports(array $config,array $context = []): bool;
/**
* Return an array which will be passed as data for the chart in the Vue element.
*/
public function getDataForWidget(array $config,array $context = []): array;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Widget;
use Chill\MainBundle\Widget\WidgetHandlerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class WidgetHandlerManager
{
private iterable $handlers;
public function __construct(
iterable $handlers
) {
$this->handlers = $handlers;
}
public function getHandler(array $config,): WidgetHandlerInterface
{
foreach ($this->handlers as $widget) {
if ($widget->supports($config)) {
return $widget;
}
}
// Handle unsupported context
throw new \InvalidArgumentException('Unsupported widget.');
}
public function getDataForWidget(array $config, array $context=[]): array
{
return $this->getHandler($config)->getDataForWidget($config,$context);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Chill\MainBundle\Widget\Widgets\DataFetcher;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
class WidgetBarDataFetcher
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* @throws \Exception
*/
public function fetchDataForWidget(int $userId): array
{
$sql = "
SELECT DATE_TRUNC('month', ap.openingdate) AS month,
COUNT(ap.id) AS count
FROM chill_person_accompanying_period ap
WHERE ap.user_id = :userId
GROUP BY DATE_TRUNC('month', ap.openingdate)
ORDER BY DATE_TRUNC('month', ap.openingdate) ASC
";
$rsm = new ResultSetMapping();
$rsm->addScalarResult('month', 'month');
$rsm->addScalarResult('count', 'count');
$query = $this->entityManager->createNativeQuery($sql, $rsm);
$query->setParameter('userId', $userId);
$results = $query->getResult();
$counts = [];
$months =[] ;
foreach ($results as $result) {
$date = new \DateTime($result['month']);
$formattedMonth = $date->format('Y-m');
$months[] = $formattedMonth;
$counts[] = $result['count'];
}
return [
'months'=>$months,
'counts'=>$counts,
];
}
}

View File

@@ -0,0 +1,105 @@
<?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\Widget\Widgets;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Widget\WidgetHandlerInterface;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Chill\MainBundle\Widget\Widgets\DataFetcher\WidgetBarDataFetcher;
class WidgetBar implements WidgetHandlerInterface
{
private Security $security;
//private AccompanyingPeriodRepository $acpRepository;
private WidgetBarDataFetcher $dataFetcher;
//private NotificationRepository $notificationRepository;
//private EntityManagerInterface $em;
public function __construct(
// AccompanyingPeriodRepository $accompanyingPeriodRepository,
Security $security,
// NotificationRepository $notificationRepository,
// EntityManagerInterface $em,
WidgetBarDataFetcher $dataFetcher
)
{
// $this->acpRepository = $accompanyingPeriodRepository;
$this->security = $security;
// $this->notificationRepository = $notificationRepository;
// $this->em = $em;
$this->dataFetcher = $dataFetcher;
}
public function supports(array $config, array $context = []): bool
{
// Check if the context is "bar"
return $config['alias'] === 'bar';
}
/**
* @throws \Exception
*/
public function getDataForWidget(array $config, array $context = []): array
{
$user = $this->security->getUser();
//$userId = $user->getId();
//Hardcoder pour résultat
$userId = 19;
$dataForWidget = [];
if (!$user instanceof User) {
throw new AccessDeniedException();
}
if ($context !== []) {
switch ($context['what']) {
case 'accompanying_period_by_month':
$data = $this->dataFetcher->fetchDataForWidget($userId);
$dataForWidget = [
'labels' => $data['months'],
'datasets' => [
[
'label' => 'Number of accompanying periods opened',
'backgroundColor' => ['#41B883'],
'data' => $data['counts']
]
]
];
break;
default:
throw new \InvalidArgumentException('Invalid Context.');
}
}
return [
'data' => $dataForWidget,
'options' => [
'responsive' => 'true',
'maintainAspectRatio' => 'false',
'position' => 'relative'
],
'type' => 'bar',
];
}
}

View File

@@ -0,0 +1,104 @@
<?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\Widget\Widgets;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Widget\WidgetHandlerInterface;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Repository\Person;
use DateInterval;
use DateTimeImmutable;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
class WidgetNumber implements WidgetHandlerInterface
{
private Security $security;
//private AccompanyingPeriodRepository $acpRepository;
private NotificationRepository $notificationRepository;
public function __construct(
// AccompanyingPeriodRepository $accompanyingPeriodRepository,
Security $security,
NotificationRepository $notificationRepository,
)
{
// $this->acpRepository = $accompanyingPeriodRepository;
$this->security = $security;
$this->notificationRepository = $notificationRepository;
}
public function supports(array $config, array $context = []): bool
{
return $config['alias'] === 'number';
}
public function getDataForWidget(array $config, array $context = []): array
{
$user = $this->security->getUser();
$dataForWidget = [];
if (!$user instanceof User) {
throw new AccessDeniedException();
}
if ($context !== []) {
switch ($context['what']) {
/*case 'accompanying_period_history' :
//Send back a data that need to be serialize in order to see
$since = (new DateTimeImmutable('now'))->sub(new DateInterval('P1Y'));
$data = $this->acpRepository->countByRecentUserHistory($user, $since);
$what = $this->acpRepository->findConfirmedByUser($user);
break;*/
case 'notification_sender':
//Count the number of notification the sender send
$countSend = $this->notificationRepository->countAllForSender($user);
$dataForWidget = [
'labels' => ['notification(s) envoyée(s)'],
'datasets' => [
[
'data' => [$countSend]
]
]
];
break;
case 'notification_unread':
//Count the number of unread notification by the current User
$countUnread = $this->notificationRepository->countUnreadByUser($user);
$dataForWidget = [
'labels' => ['notification(s) non lue(s)'],
'datasets' => [
[
'data' => [$countUnread]
]
]
];
break;
default : throw new \InvalidArgumentException('Invalid Context.');
}
}
return [
'data' => $dataForWidget,
'type' => 'number',
];
}
}

View File

@@ -10,6 +10,22 @@ servers:
components:
schemas:
SavedExport:
type: object
properties:
id:
type: integer
user_id:
type: integer
description:
type: string
exportalias:
type: string
options:
type: string #TODO -> je pense que c'est object
title:
type: string
User:
type: object
properties:
@@ -843,3 +859,27 @@ paths:
403:
description: "Unauthorized"
/1.0/main/saved-export/{id}.json:
get:
tags:
- export
summary: Return a specific saved export who is saved by an existing user.
parameters:
- name: id
in: path
required: true
description: The saved export id
schema:
type: string
format: string
responses:
200:
description: "ok"
content:
application/json:
schema:
type: string
items:
$ref: '#/components/schemas/SavedExport'
403:
description: "Unauthorized"

View File

@@ -115,6 +115,24 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Widget\:
resource: '../Widget/'
autowire: true
autoconfigure: true
Chill\MainBundle\Widget\WidgetHandlerManager:
arguments:
$handlers: !tagged_iterator chill_main.widget_handler
Chill\MainBundle\Widget\Widgets\WidgetNumber:
autoconfigure: true
autowire: true
Chill\MainBundle\Widget\Widgets\WidgetBar:
autoconfigure: true
autowire: true
Chill\MainBundle\Cron\CronManager:
autoconfigure: true
autowire: true

View File

@@ -112,4 +112,17 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $qb;
}
public function countByUserGroupedByMonth(User $user): array
{
$qb = $this->createQueryBuilder('a');
$qb->select()
->addSelect('COUNT(a.id) AS count')
->andWhere('a.user = :user')
->setParameter('user', $user)
->groupBy('month')
->orderBy('month', 'ASC');
return $qb->getQuery()->getResult();
}
}