feat : ajout des notifications
This commit is contained in:
@@ -132,3 +132,16 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
||||
- uniquement prénom, nom, site
|
||||
- pas de modification de contrat depuis ce drawer
|
||||
|
||||
## 10) Notifications
|
||||
|
||||
- Icône cloche en topbar:
|
||||
- badge = nombre de notifications non lues
|
||||
- ouverture panneau = liste des non lues
|
||||
- fermeture panneau = marquage "lu" en masse
|
||||
|
||||
### Règle métier de déclenchement
|
||||
|
||||
- Les notifications de validation site ne sont pas envoyées ligne par ligne.
|
||||
- Une notification est créée uniquement quand un chef de site termine la validation complète:
|
||||
- condition: plus aucune ligne `work_hours` du site à la date concernée avec `isSiteValid = false`
|
||||
- destinataires: utilisateurs `ROLE_ADMIN`
|
||||
|
||||
@@ -2,7 +2,42 @@
|
||||
<header class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
||||
<div class="flex h-full items-center justify-end">
|
||||
<div class="flex gap-12 text-xl text-white">
|
||||
<Icon name="mdi:bell-plus" class="self-center cursor-pointer" size="36" />
|
||||
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
||||
<button type="button" class="relative self-center cursor-pointer" @click="toggleNotifications">
|
||||
<Icon name="mdi:bell-plus" size="36" />
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="absolute -right-1 -top-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-xs font-bold text-white"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="isNotificationsOpen"
|
||||
class="absolute right-0 top-full z-30 mt-2 w-80 rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
||||
>
|
||||
<div class="border-b border-neutral-200 px-3 py-2 text-sm font-semibold">
|
||||
Notifications
|
||||
</div>
|
||||
<div v-if="isLoadingNotifications" class="px-3 py-3 text-sm text-neutral-500">
|
||||
Chargement...
|
||||
</div>
|
||||
<div v-else-if="notifications.length === 0" class="px-3 py-3 text-sm text-neutral-500">
|
||||
Aucune notification.
|
||||
</div>
|
||||
<div v-else class="max-h-80 overflow-auto">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="border-b border-neutral-100 px-3 py-2 last:border-b-0"
|
||||
>
|
||||
<p class="text-sm font-semibold text-neutral-900">{{ notification.title }}</p>
|
||||
<p class="text-xs text-neutral-600">{{ notification.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group relative flex gap-4">
|
||||
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
|
||||
<p class="self-center cursor-pointer">{{ user?.username }}</p>
|
||||
@@ -29,15 +64,79 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { User } from '~/services/dto/user'
|
||||
import type { NotificationItem } from '~/services/dto/notification'
|
||||
import { listUnreadNotifications, markAllNotificationsRead } from '~/services/notifications'
|
||||
|
||||
defineProps<{
|
||||
user?: User
|
||||
}>()
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const bellRoot = ref<HTMLElement | null>(null)
|
||||
const notifications = ref<NotificationItem[]>([])
|
||||
const isNotificationsOpen = ref(false)
|
||||
const isLoadingNotifications = ref(false)
|
||||
const unreadCount = computed(() => notifications.value.length)
|
||||
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
await auth.logout()
|
||||
await navigateTo('/login')
|
||||
}
|
||||
|
||||
const loadNotifications = async () => {
|
||||
isLoadingNotifications.value = true
|
||||
try {
|
||||
notifications.value = await listUnreadNotifications()
|
||||
} finally {
|
||||
isLoadingNotifications.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeNotifications = async () => {
|
||||
if (!isNotificationsOpen.value) return
|
||||
isNotificationsOpen.value = false
|
||||
if (notifications.value.length > 0) {
|
||||
await markAllNotificationsRead()
|
||||
notifications.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNotifications = async () => {
|
||||
if (isNotificationsOpen.value) {
|
||||
await closeNotifications()
|
||||
return
|
||||
}
|
||||
|
||||
isNotificationsOpen.value = true
|
||||
await loadNotifications()
|
||||
}
|
||||
|
||||
const handleClickOutside = async (event: MouseEvent) => {
|
||||
const target = event.target as Node | null
|
||||
if (!target || !bellRoot.value) return
|
||||
if (!bellRoot.value.contains(target)) {
|
||||
await closeNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isAdmin.value) {
|
||||
await loadNotifications()
|
||||
}
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
if (!isAdmin.value) return
|
||||
await loadNotifications()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineNuxtConfig({
|
||||
devServer: {port: 3001},
|
||||
toast: {
|
||||
settings: {
|
||||
timeout: 10000,
|
||||
timeout: 2000,
|
||||
closeOnClick: true,
|
||||
progressBar: false
|
||||
}
|
||||
|
||||
7
frontend/services/dto/notification.ts
Normal file
7
frontend/services/dto/notification.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type NotificationItem = {
|
||||
id: number
|
||||
title: string
|
||||
message: string
|
||||
isRead: boolean
|
||||
createdAt: string
|
||||
}
|
||||
18
frontend/services/notifications.ts
Normal file
18
frontend/services/notifications.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NotificationItem } from './dto/notification'
|
||||
import { extractItems } from '~/utils/api'
|
||||
|
||||
export const listUnreadNotifications = async () => {
|
||||
const api = useApi()
|
||||
const data = await api.get<NotificationItem[] | { 'hydra:member'?: NotificationItem[] }>(
|
||||
'/notifications/unread',
|
||||
{},
|
||||
{ toast: false }
|
||||
)
|
||||
|
||||
return extractItems<NotificationItem>(data)
|
||||
}
|
||||
|
||||
export const markAllNotificationsRead = async () => {
|
||||
const api = useApi()
|
||||
return api.post('/notifications/mark-all-read', {}, { toast: false })
|
||||
}
|
||||
30
migrations/Version20260302110000.php
Normal file
30
migrations/Version20260302110000.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260302110000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add notifications table for user notification center';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE notifications (id SERIAL NOT NULL, recipient_id INT NOT NULL, title VARCHAR(120) NOT NULL, message TEXT NOT NULL, is_read BOOLEAN DEFAULT FALSE NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX idx_notifications_recipient_read_created ON notifications (recipient_id, is_read, created_at)');
|
||||
$this->addSql('CREATE INDEX IDX_6000B0D0E92F8F78 ON notifications (recipient_id)');
|
||||
$this->addSql('ALTER TABLE notifications ADD CONSTRAINT FK_6000B0D0E92F8F78 FOREIGN KEY (recipient_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE notifications DROP CONSTRAINT FK_6000B0D0E92F8F78');
|
||||
$this->addSql('DROP TABLE notifications');
|
||||
}
|
||||
}
|
||||
134
src/Entity/Notification.php
Normal file
134
src/Entity/Notification.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\NotificationRepository;
|
||||
use App\State\MarkAllNotificationsReadProcessor;
|
||||
use App\State\UnreadNotificationsProvider;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/notifications/unread',
|
||||
normalizationContext: ['groups' => ['notification:read']],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: UnreadNotificationsProvider::class,
|
||||
paginationEnabled: false
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/notifications/mark-all-read',
|
||||
security: "is_granted('ROLE_USER')",
|
||||
input: false,
|
||||
output: false,
|
||||
read: false,
|
||||
processor: MarkAllNotificationsReadProcessor::class
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: NotificationRepository::class)]
|
||||
#[ORM\Table(name: 'notifications')]
|
||||
#[ORM\Index(columns: ['recipient_id', 'is_read', 'created_at'], name: 'idx_notifications_recipient_read_created')]
|
||||
class Notification
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Groups(['notification:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
private ?User $recipient = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 120)]
|
||||
#[Groups(['notification:read'])]
|
||||
private string $title = '';
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
#[Groups(['notification:read'])]
|
||||
private string $message = '';
|
||||
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => false])]
|
||||
#[Groups(['notification:read'])]
|
||||
private bool $isRead = false;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
#[Groups(['notification:read'])]
|
||||
private DateTimeImmutable $createdAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRecipient(): ?User
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
public function setRecipient(?User $recipient): self
|
||||
{
|
||||
$this->recipient = $recipient;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRead(): bool
|
||||
{
|
||||
return $this->isRead;
|
||||
}
|
||||
|
||||
public function getIsRead(): bool
|
||||
{
|
||||
return $this->isRead;
|
||||
}
|
||||
|
||||
public function setIsRead(bool $isRead): self
|
||||
{
|
||||
$this->isRead = $isRead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\UserRepository;
|
||||
use App\State\CurrentUserProvider;
|
||||
use App\State\UserPasswordHasherProcessor;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@@ -52,7 +53,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity]
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: 'users')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_users_username', fields: ['username'])]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
53
src/Repository/NotificationRepository.php
Normal file
53
src/Repository/NotificationRepository.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Notification;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Notification>
|
||||
*/
|
||||
final class NotificationRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Notification::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Notification>
|
||||
*/
|
||||
public function findUnreadByRecipient(User $recipient): array
|
||||
{
|
||||
return $this->createQueryBuilder('n')
|
||||
->andWhere('n.recipient = :recipient')
|
||||
->andWhere('n.isRead = :isRead')
|
||||
->setParameter('recipient', $recipient)
|
||||
->setParameter('isRead', false)
|
||||
->orderBy('n.createdAt', 'DESC')
|
||||
->setMaxResults(50)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function markAllReadByRecipient(User $recipient): int
|
||||
{
|
||||
return $this->createQueryBuilder('n')
|
||||
->update()
|
||||
->set('n.isRead', ':isRead')
|
||||
->andWhere('n.recipient = :recipient')
|
||||
->andWhere('n.isRead = :current')
|
||||
->setParameter('isRead', true)
|
||||
->setParameter('current', false)
|
||||
->setParameter('recipient', $recipient)
|
||||
->getQuery()
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
}
|
||||
38
src/Repository/UserRepository.php
Normal file
38
src/Repository/UserRepository.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*/
|
||||
final class UserRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<User>
|
||||
*/
|
||||
public function findAllAdmins(): array
|
||||
{
|
||||
/** @var list<User> $users */
|
||||
$users = $this->createQueryBuilder('u')
|
||||
->orderBy('u.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
return array_values(array_filter(
|
||||
$users,
|
||||
static fn (User $user): bool => in_array('ROLE_ADMIN', $user->getRoles(), true)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -137,4 +137,23 @@ final class WorkHourRepository extends ServiceEntityRepository implements WorkHo
|
||||
// @var null|WorkHour $workHour
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function hasPendingSiteValidationForSiteAndDate(int $siteId, DateTimeInterface $date): bool
|
||||
{
|
||||
$workDate = DateTimeImmutable::createFromInterface($date);
|
||||
|
||||
$qb = $this->createQueryBuilder('w')
|
||||
->select('COUNT(w.id)')
|
||||
->leftJoin('w.employee', 'e')
|
||||
->leftJoin('e.site', 's')
|
||||
->andWhere('s.id = :siteId')
|
||||
->andWhere('w.workDate = :workDate')
|
||||
->andWhere('w.isSiteValid = :isSiteValid')
|
||||
->setParameter('siteId', $siteId)
|
||||
->setParameter('workDate', $workDate)
|
||||
->setParameter('isSiteValid', false)
|
||||
;
|
||||
|
||||
return ((int) $qb->getQuery()->getSingleScalarResult()) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
32
src/State/MarkAllNotificationsReadProcessor.php
Normal file
32
src/State/MarkAllNotificationsReadProcessor.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\User;
|
||||
use App\Repository\NotificationRepository;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
final readonly class MarkAllNotificationsReadProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private NotificationRepository $notificationRepository,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): null
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Authentication required.');
|
||||
}
|
||||
|
||||
$this->notificationRepository->markAllReadByRecipient($user);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
30
src/State/UnreadNotificationsProvider.php
Normal file
30
src/State/UnreadNotificationsProvider.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\User;
|
||||
use App\Repository\NotificationRepository;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
final readonly class UnreadNotificationsProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private NotificationRepository $notificationRepository,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Authentication required.');
|
||||
}
|
||||
|
||||
return $this->notificationRepository->findUnreadByRecipient($user);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\Notification;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Security\EmployeeScopeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
@@ -18,6 +21,8 @@ final readonly class WorkHourSiteValidationProcessor implements ProcessorInterfa
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private EmployeeScopeService $employeeScopeService,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private UserRepository $userRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
@@ -47,8 +52,38 @@ final readonly class WorkHourSiteValidationProcessor implements ProcessorInterfa
|
||||
throw new AccessDeniedHttpException('Employee is outside your site scope.');
|
||||
}
|
||||
|
||||
$uow = $this->entityManager->getUnitOfWork();
|
||||
$uow->computeChangeSets();
|
||||
$changeSet = $uow->getEntityChangeSet($data);
|
||||
$isSiteValidationChangedToTrue = isset($changeSet['isSiteValid'])
|
||||
&& false === $changeSet['isSiteValid'][0]
|
||||
&& true === $changeSet['isSiteValid'][1];
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Notification uniquement quand la dernière ligne du site est validée pour la date.
|
||||
if ($isSiteValidationChangedToTrue) {
|
||||
$workDate = $data->getWorkDate();
|
||||
$hasPending = $this->workHourRepository->hasPendingSiteValidationForSiteAndDate($siteId, $workDate);
|
||||
if (!$hasPending) {
|
||||
$siteName = $data->getEmployee()?->getSite()?->getName() ?? 'Site';
|
||||
$dateLabel = $workDate->format('d/m/Y');
|
||||
$title = sprintf('%s validé', $siteName);
|
||||
$message = sprintf('Le site %s a terminé la validation du %s.', $siteName, $dateLabel);
|
||||
|
||||
foreach ($this->userRepository->findAllAdmins() as $admin) {
|
||||
$notification = new Notification()
|
||||
->setRecipient($admin)
|
||||
->setTitle($title)
|
||||
->setMessage($message)
|
||||
;
|
||||
$this->entityManager->persist($notification);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user