feat(api) : add machine count to category related items endpoint

This commit is contained in:
2026-04-04 17:29:39 +02:00
parent 8b02f821d3
commit 14ed38704f
2 changed files with 191 additions and 1 deletions

View File

@@ -48,6 +48,39 @@
<span class="whitespace-nowrap">{{ formatDate(row.createdAt) }}</span>
</template>
<template #cell-composantCount="{ row }">
<NuxtLink
v-if="stats[row.id]?.composantCount"
:to="`/catalogues/composants?constructeur=${row.id}`"
class="badge badge-ghost badge-sm hover:badge-primary transition-colors"
>
{{ stats[row.id].composantCount }}
</NuxtLink>
<span v-else class="text-base-content/30"></span>
</template>
<template #cell-pieceCount="{ row }">
<NuxtLink
v-if="stats[row.id]?.pieceCount"
:to="`/catalogues/pieces?constructeur=${row.id}`"
class="badge badge-ghost badge-sm hover:badge-primary transition-colors"
>
{{ stats[row.id].pieceCount }}
</NuxtLink>
<span v-else class="text-base-content/30"></span>
</template>
<template #cell-machineCount="{ row }">
<NuxtLink
v-if="stats[row.id]?.machineCount"
:to="`/machines?constructeur=${row.id}`"
class="badge badge-ghost badge-sm hover:badge-primary transition-colors"
>
{{ stats[row.id].machineCount }}
</NuxtLink>
<span v-else class="text-base-content/30"></span>
</template>
<template #cell-actions="{ row }">
<div class="flex items-center justify-end gap-2">
<button class="btn btn-ghost btn-xs" @click="openEditModal(row)">
@@ -103,6 +136,7 @@ import { formatPhone } from '~/utils/formatters/phone'
import { formatFrenchDate } from '~/utils/date'
import IconLucidePlus from '~icons/lucide/plus'
const api = useApi()
const { canEdit } = usePermissions()
const { constructeurs, loading, searchConstructeurs, createConstructeur, updateConstructeur, deleteConstructeur, loadConstructeurs } = useConstructeurs()
const { showError } = useToast()
@@ -112,12 +146,16 @@ const columns = [
{ key: 'email', label: 'Email', sortable: true },
{ key: 'phone', label: 'Téléphone', sortable: true },
{ key: 'createdAt', label: 'Date de création', sortable: true },
{ key: 'composantCount', label: 'Composants', align: 'center' },
{ key: 'pieceCount', label: 'Pièces', align: 'center' },
{ key: 'machineCount', label: 'Machines', align: 'center' },
{ key: 'actions', label: 'Actions', align: 'right' },
]
const searchTerm = ref('')
const sortKey = usePersistedValue('constructeurs-sort', 'name')
const sortDir = ref('asc')
const stats = ref({})
const currentSort = computed(() => ({
field: sortKey.value,
@@ -236,5 +274,15 @@ const confirmDelete = async (constructeur) => {
}
}
onMounted(() => loadConstructeurs())
const loadStats = async () => {
const result = await api.get('/constructeurs/stats')
if (result.success && result.data) {
stats.value = result.data
}
}
onMounted(() => {
loadConstructeurs()
loadStats()
})
</script>

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Composant;
use App\Entity\MachineComponentLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\Piece;
use App\Entity\Product;
use App\Enum\ModelCategory;
use App\Repository\ModelTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/model_types/{id}')]
final class ModelTypeRelatedItemsController extends AbstractController
{
public function __construct(
private readonly ModelTypeRepository $modelTypes,
private readonly EntityManagerInterface $em,
) {}
#[Route('/related-items', name: 'api_model_type_related_items', methods: ['GET'])]
public function relatedItems(string $id): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
$modelType = $this->modelTypes->find($id);
if (!$modelType) {
return new JsonResponse(
['message' => 'Catégorie introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$items = match ($modelType->getCategory()) {
ModelCategory::COMPONENT => $this->fetchComposantsWithMachineCount($id),
ModelCategory::PIECE => $this->fetchPiecesWithMachineCount($id),
ModelCategory::PRODUCT => $this->fetchProductsWithMachineCount($id),
};
return new JsonResponse($items);
}
/**
* @return list<array{id: string, name: string, reference: null|string, machineCount: int}>
*/
private function fetchComposantsWithMachineCount(string $modelTypeId): array
{
$qb = $this->em->createQueryBuilder();
$qb->select(
'c.id',
'c.name',
'c.reference',
'COALESCE(c.referenceAuto, c.reference) as displayReference',
'COUNT(DISTINCT mcl.id) as machineCount',
)
->from(Composant::class, 'c')
->leftJoin(MachineComponentLink::class, 'mcl', 'WITH', 'mcl.composant = c')
->where('c.typeComposant = :modelTypeId')
->setParameter('modelTypeId', $modelTypeId)
->groupBy('c.id', 'c.name', 'c.reference', 'c.referenceAuto')
->orderBy('c.name', 'ASC')
;
return $this->formatResults($qb->getQuery()->getArrayResult());
}
/**
* @return list<array{id: string, name: string, reference: null|string, machineCount: int}>
*/
private function fetchPiecesWithMachineCount(string $modelTypeId): array
{
$qb = $this->em->createQueryBuilder();
$qb->select(
'p.id',
'p.name',
'p.reference',
'COALESCE(p.referenceAuto, p.reference) as displayReference',
'COUNT(DISTINCT mpl.id) as machineCount',
)
->from(Piece::class, 'p')
->leftJoin(MachinePieceLink::class, 'mpl', 'WITH', 'mpl.piece = p')
->where('p.typePiece = :modelTypeId')
->setParameter('modelTypeId', $modelTypeId)
->groupBy('p.id', 'p.name', 'p.reference', 'p.referenceAuto')
->orderBy('p.name', 'ASC')
;
return $this->formatResults($qb->getQuery()->getArrayResult());
}
/**
* @return list<array{id: string, name: string, reference: null|string, machineCount: int}>
*/
private function fetchProductsWithMachineCount(string $modelTypeId): array
{
$qb = $this->em->createQueryBuilder();
$qb->select(
'pr.id',
'pr.name',
'pr.reference',
'COUNT(DISTINCT mpl.id) as machineCount',
)
->from(Product::class, 'pr')
->leftJoin(MachineProductLink::class, 'mpl', 'WITH', 'mpl.product = pr')
->where('pr.typeProduct = :modelTypeId')
->setParameter('modelTypeId', $modelTypeId)
->groupBy('pr.id', 'pr.name', 'pr.reference')
->orderBy('pr.name', 'ASC')
;
return $this->formatResults($qb->getQuery()->getArrayResult());
}
/**
* @param array<int, array<string, mixed>> $rows
*
* @return list<array{id: string, name: string, reference: null|string, machineCount: int}>
*/
private function formatResults(array $rows): array
{
return array_values(array_map(
static fn (array $row): array => [
'id' => (string) $row['id'],
'name' => (string) ($row['name'] ?? ''),
'reference' => isset($row['displayReference']) && '' !== $row['displayReference']
? (string) $row['displayReference']
: (isset($row['reference']) && '' !== $row['reference'] ? (string) $row['reference'] : null),
'machineCount' => (int) $row['machineCount'],
],
$rows,
));
}
}