Compare commits
9 Commits
508066d39f
...
v1.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c |
Submodule Inventory_frontend updated: 7c44778f25...675820532c
@@ -5,3 +5,5 @@ api_platform:
|
|||||||
stateless: false
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
pagination_items_per_page: 30
|
||||||
|
pagination_maximum_items_per_page: 200
|
||||||
|
|||||||
87
src/Controller/ActivityLogController.php
Normal file
87
src/Controller/ActivityLogController.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class ActivityLogController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$page = max(1, $request->query->getInt('page', 1));
|
||||||
|
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));
|
||||||
|
|
||||||
|
$filters = [];
|
||||||
|
if ($entityType = $request->query->get('entityType')) {
|
||||||
|
$filters['entityType'] = $entityType;
|
||||||
|
}
|
||||||
|
if ($action = $request->query->get('action')) {
|
||||||
|
$filters['action'] = $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->auditLogs->findAllPaginated($page, $itemsPerPage, $filters);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$result['items'],
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
$snapshot = $log->getSnapshot();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'entityType' => $log->getEntityType(),
|
||||||
|
'entityId' => $log->getEntityId(),
|
||||||
|
'entityName' => $snapshot['name'] ?? null,
|
||||||
|
'entityRef' => $snapshot['reference'] ?? null,
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $snapshot,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$result['items'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $page,
|
||||||
|
'itemsPerPage' => $itemsPerPage,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
normalizationContext: ['groups' => ['composant:read']],
|
normalizationContext: ['groups' => ['composant:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Composant
|
class Composant
|
||||||
{
|
{
|
||||||
@@ -144,7 +144,7 @@ class Composant
|
|||||||
|
|
||||||
public function setName(string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Constructeur
|
class Constructeur
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
use App\Repository\DocumentRepository;
|
use App\Repository\DocumentRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
@@ -13,7 +14,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
||||||
#[ORM\Table(name: 'documents')]
|
#[ORM\Table(name: 'documents')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiResource]
|
#[ApiResource(
|
||||||
|
paginationClientItemsPerPage: true,
|
||||||
|
paginationMaximumItemsPerPage: 200
|
||||||
|
)]
|
||||||
class Document
|
class Document
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -62,19 +66,19 @@ class Document
|
|||||||
private ?Site $site = null;
|
private ?Site $site = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
|
||||||
private \DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
|
||||||
private \DateTimeImmutable $updatedAt;
|
private DateTimeImmutable $updatedAt;
|
||||||
|
|
||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function setCreatedAtValue(): void
|
public function setCreatedAtValue(): void
|
||||||
{
|
{
|
||||||
$now = new \DateTimeImmutable();
|
$now = new DateTimeImmutable();
|
||||||
$this->createdAt = $now;
|
$this->createdAt = $now;
|
||||||
$this->updatedAt = $now;
|
$this->updatedAt = $now;
|
||||||
|
|
||||||
if ($this->id === null) {
|
if (null === $this->id) {
|
||||||
$this->id = $this->generateCuid();
|
$this->id = $this->generateCuid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,12 +86,7 @@ class Document
|
|||||||
#[ORM\PreUpdate]
|
#[ORM\PreUpdate]
|
||||||
public function setUpdatedAtValue(): void
|
public function setUpdatedAtValue(): void
|
||||||
{
|
{
|
||||||
$this->updatedAt = new \DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
|
||||||
|
|
||||||
private function generateCuid(): string
|
|
||||||
{
|
|
||||||
return 'cl' . bin2hex(random_bytes(12));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?string
|
public function getId(): ?string
|
||||||
@@ -222,13 +221,18 @@ class Document
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedAt(): \DateTimeImmutable
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUpdatedAt(): \DateTimeImmutable
|
public function getUpdatedAt(): DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->updatedAt;
|
return $this->updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCuid(): string
|
||||||
|
{
|
||||||
|
return 'cl'.bin2hex(random_bytes(12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
@@ -21,9 +22,10 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||||||
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class ModelType
|
class ModelType
|
||||||
{
|
{
|
||||||
@@ -178,7 +180,7 @@ class ModelType
|
|||||||
|
|
||||||
public function setName(string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
normalizationContext: ['groups' => ['piece:read']],
|
normalizationContext: ['groups' => ['piece:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Piece
|
class Piece
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
normalizationContext: ['groups' => ['product:read']],
|
normalizationContext: ['groups' => ['product:read']],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Product
|
class Product
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
new Delete(),
|
new Delete(),
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class Site
|
class Site
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
new Delete(),
|
new Delete(),
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 500
|
paginationMaximumItemsPerPage: 200
|
||||||
)]
|
)]
|
||||||
class TypeMachine
|
class TypeMachine
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -293,23 +293,28 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (is_object($item) && method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if (null !== $id && '' !== $id) {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
|||||||
@@ -293,23 +293,28 @@ final class PieceAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (is_object($item) && method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if (null !== $id && '' !== $id) {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
|||||||
@@ -315,23 +315,28 @@ final class ProductAuditSubscriber implements EventSubscriber
|
|||||||
/**
|
/**
|
||||||
* @param iterable<mixed> $items
|
* @param iterable<mixed> $items
|
||||||
*
|
*
|
||||||
* @return list<string>
|
* @return list<array{id: string, name: string}|string>
|
||||||
*/
|
*/
|
||||||
private function normalizeCollection(iterable $items): array
|
private function normalizeCollection(iterable $items): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
if (is_object($item) && method_exists($item, 'getId')) {
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
$id = $item->getId();
|
$id = $item->getId();
|
||||||
if (null !== $id && '' !== $id) {
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
$ids[] = (string) $id;
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort($ids);
|
return $entries;
|
||||||
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeValue(mixed $value): mixed
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
|||||||
@@ -31,7 +31,46 @@ final class AuditLogRepository extends ServiceEntityRepository
|
|||||||
->orderBy('a.createdAt', 'DESC')
|
->orderBy('a.createdAt', 'DESC')
|
||||||
->setMaxResults($limit)
|
->setMaxResults($limit)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{entityType?: string, action?: string} $filters
|
||||||
|
*
|
||||||
|
* @return array{items: list<AuditLog>, total: int}
|
||||||
|
*/
|
||||||
|
public function findAllPaginated(int $page = 1, int $itemsPerPage = 30, array $filters = []): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('a')
|
||||||
|
->orderBy('a.createdAt', 'DESC')
|
||||||
|
;
|
||||||
|
|
||||||
|
if (!empty($filters['entityType'])) {
|
||||||
|
$qb->andWhere('a.entityType = :entityType')
|
||||||
|
->setParameter('entityType', $filters['entityType'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['action'])) {
|
||||||
|
$qb->andWhere('a.action = :action')
|
||||||
|
->setParameter('action', $filters['action'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQb = clone $qb;
|
||||||
|
$countQb->select('COUNT(a.id)')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
;
|
||||||
|
$total = (int) $countQb->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
$qb->setFirstResult(($page - 1) * $itemsPerPage)
|
||||||
|
->setMaxResults($itemsPerPage)
|
||||||
|
;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $qb->getQuery()->getResult(),
|
||||||
|
'total' => $total,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user