fix : code review — correct 15 issues across UX overhaul (phases 1-4)
Critical fixes: - Make MigrateConstructeurLinks migration no-op (legacy tables already dropped) - Add explicit ON CONFLICT (id) target in RestoreConstructeurLinks migration - Replace N+1 queries with 4 bulk GROUP BY in ConstructeurStatsController - Declare missing versionListRef template ref in machine detail page - Add missing await on removeMachineDocument, cast activeTab as string Important fixes: - Add lang="ts" to ToastContainer and constructeurs page - Type entityType as union in UsedInSection/useUsedIn - Remove dead duration param from showError - Update back-link props to new /catalogues/* URLs (3 pages) - Replace raw error blocks with EmptyState in component/piece detail pages - Type handleFillEntity params and machineInfoCardRef Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import IconLucideCheck from '~icons/lucide/check'
|
import IconLucideCheck from '~icons/lucide/check'
|
||||||
import IconLucideX from '~icons/lucide/x'
|
import IconLucideX from '~icons/lucide/x'
|
||||||
@@ -77,7 +77,7 @@ import IconLucideInfo from '~icons/lucide/info'
|
|||||||
|
|
||||||
const { toasts, removeToast } = useToast()
|
const { toasts, removeToast } = useToast()
|
||||||
|
|
||||||
const getToastClasses = (type) => {
|
const getToastClasses = (type: ToastType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'alert-success text-success-content'
|
return 'alert-success text-success-content'
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
entityType: string
|
entityType: 'composants' | 'pieces' | 'products'
|
||||||
entityId: string | null
|
entityId: string | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export function useToast() {
|
|||||||
return showToast(message, 'success', duration)
|
return showToast(message, 'success', duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showError = (message: string, duration = 5000): number => {
|
const showError = (message: string): number => {
|
||||||
return showToast(message, 'error', duration)
|
return showToast(message, 'error', 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showWarning = (message: string, duration = 6000): number => {
|
const showWarning = (message: string, duration = 6000): number => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface UsedInData {
|
|||||||
pieces: UsedInEntity[]
|
pieces: UsedInEntity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsedIn(entityType: Ref<string>, entityId: Ref<string | null>) {
|
export function useUsedIn(entityType: Ref<'composants' | 'pieces' | 'products'>, entityId: Ref<string | null>) {
|
||||||
const data = ref<UsedInData>({ machines: [], composants: [], pieces: [] })
|
const data = ref<UsedInData>({ machines: [], composants: [], pieces: [] })
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -18,19 +18,13 @@
|
|||||||
<p class="text-sm text-base-content/70">Chargement du composant…</p>
|
<p class="text-sm text-base-content/70">Chargement du composant…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!component" class="max-w-xl mx-auto">
|
<EmptyState
|
||||||
<div class="alert alert-error shadow-lg">
|
v-else-if="!component"
|
||||||
<div>
|
title="Composant introuvable"
|
||||||
<h2 class="font-semibold text-lg">Composant introuvable</h2>
|
description="Nous n'avons pas pu retrouver le composant demandé. Il a peut-être été supprimé."
|
||||||
<p class="text-sm text-base-content/80">
|
action-label="Retour au catalogue"
|
||||||
Nous n'avons pas pu retrouver le composant demandé. Il a peut-être été supprimé.
|
action-to="/catalogues/composants"
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-primary mt-6" @click="$router.back()">
|
|
||||||
Retour au catalogue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
||||||
<div class="card-body space-y-6">
|
<div class="card-body space-y-6">
|
||||||
@@ -39,7 +33,7 @@
|
|||||||
:subtitle="isEditMode ? 'Ajustez les informations du composant et ses champs personnalisés.' : undefined"
|
:subtitle="isEditMode ? 'Ajustez les informations du composant et ses champs personnalisés.' : undefined"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
:can-edit="canEdit"
|
:can-edit="canEdit"
|
||||||
back-link="/component-catalog"
|
back-link="/catalogues/composants"
|
||||||
@toggle-edit="isEditMode = !isEditMode"
|
@toggle-edit="isEditMode = !isEditMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -129,6 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Référence auto (read-only, shown only if computed) -->
|
||||||
|
<div v-if="component.referenceAuto" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Référence auto</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-sm font-medium text-base-content py-1 flex items-center gap-2">
|
||||||
|
<span class="font-mono font-semibold">{{ component.referenceAuto }}</span>
|
||||||
|
<span class="badge badge-sm badge-ghost">auto</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
||||||
<div
|
<div
|
||||||
v-if="isEditMode || component.reference || constructeurLinks.length"
|
v-if="isEditMode || component.reference || constructeurLinks.length"
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import DataTable from '~/components/common/DataTable.vue'
|
import DataTable from '~/components/common/DataTable.vue'
|
||||||
import FieldEmail from '~/components/form/FieldEmail.vue'
|
import FieldEmail from '~/components/form/FieldEmail.vue'
|
||||||
|
|||||||
@@ -223,12 +223,13 @@ if (!machineId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const d = useMachineDetailData(machineId)
|
const d = useMachineDetailData(machineId)
|
||||||
const machineInfoCardRef = ref(null)
|
const machineInfoCardRef = ref<{ saveFieldDefinitions?: () => Promise<void> } | null>(null)
|
||||||
const versionRefreshKey = ref(0)
|
const versionRefreshKey = ref(0)
|
||||||
const refreshVersions = () => { versionRefreshKey.value++ }
|
const refreshVersions = () => { versionRefreshKey.value++ }
|
||||||
const { confirm: confirmDialog } = useConfirm()
|
const { confirm: confirmDialog } = useConfirm()
|
||||||
|
|
||||||
const activeTab = ref(route.query.tab || 'general')
|
const versionListRef = ref<InstanceType<typeof EntityVersionList> | null>(null)
|
||||||
|
const activeTab = ref((route.query.tab as string) || 'general')
|
||||||
watch(activeTab, (val) => {
|
watch(activeTab, (val) => {
|
||||||
navigateTo({ query: { ...route.query, tab: val } }, { replace: true })
|
navigateTo({ query: { ...route.query, tab: val } }, { replace: true })
|
||||||
})
|
})
|
||||||
@@ -306,7 +307,7 @@ const handleAddEntity = async (payload) => {
|
|||||||
refreshVersions()
|
refreshVersions()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFillEntity = (linkId, entityKind, modelTypeId) => {
|
const handleFillEntity = (linkId: string, entityKind: string, modelTypeId: string) => {
|
||||||
fillLinkId.value = linkId
|
fillLinkId.value = linkId
|
||||||
fillTypeId.value = modelTypeId
|
fillTypeId.value = modelTypeId
|
||||||
addModalKind.value = entityKind
|
addModalKind.value = entityKind
|
||||||
@@ -341,7 +342,7 @@ const confirmRemovePiece = async (id: string) => {
|
|||||||
|
|
||||||
const confirmRemoveDocument = async (id: string) => {
|
const confirmRemoveDocument = async (id: string) => {
|
||||||
if (!await confirmDialog({ title: 'Supprimer ce document ?', message: 'Le fichier sera supprimé définitivement.', confirmText: 'Supprimer', dangerous: true })) return
|
if (!await confirmDialog({ title: 'Supprimer ce document ?', message: 'Le fichier sera supprimé définitivement.', confirmText: 'Supprimer', dangerous: true })) return
|
||||||
d.removeMachineDocument(id)
|
await d.removeMachineDocument(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -18,19 +18,13 @@
|
|||||||
<p class="text-sm text-base-content/70">Chargement de la pièce…</p>
|
<p class="text-sm text-base-content/70">Chargement de la pièce…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!piece" class="max-w-xl mx-auto">
|
<EmptyState
|
||||||
<div class="alert alert-error shadow-lg">
|
v-else-if="!piece"
|
||||||
<div>
|
title="Pièce introuvable"
|
||||||
<h2 class="font-semibold text-lg">Pièce introuvable</h2>
|
description="Nous n'avons pas pu retrouver la pièce demandée. Elle a peut-être été supprimée."
|
||||||
<p class="text-sm text-base-content/80">
|
action-label="Retour au catalogue"
|
||||||
Nous n'avons pas pu retrouver la pièce demandée. Elle a peut-être été supprimée.
|
action-to="/catalogues/pieces"
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-primary mt-6" @click="$router.back()">
|
|
||||||
Retour au catalogue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
||||||
<div class="card-body space-y-6">
|
<div class="card-body space-y-6">
|
||||||
@@ -39,7 +33,7 @@
|
|||||||
:subtitle="isEditMode ? 'Ajustez les informations de la pièce et ses champs personnalisés.' : undefined"
|
:subtitle="isEditMode ? 'Ajustez les informations de la pièce et ses champs personnalisés.' : undefined"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
:can-edit="canEdit"
|
:can-edit="canEdit"
|
||||||
back-link="/pieces-catalog"
|
back-link="/catalogues/pieces"
|
||||||
@toggle-edit="isEditMode = !isEditMode"
|
@toggle-edit="isEditMode = !isEditMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
:subtitle="isEditMode ? 'Ajustez les informations du produit et ses champs personnalisés.' : undefined"
|
:subtitle="isEditMode ? 'Ajustez les informations du produit et ses champs personnalisés.' : undefined"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
:can-edit="canEdit"
|
:can-edit="canEdit"
|
||||||
back-link="/product-catalog"
|
back-link="/catalogues/produits"
|
||||||
@toggle-edit="isEditMode = !isEditMode"
|
@toggle-edit="isEditMode = !isEditMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -21,69 +21,13 @@ final class Version20260405_MigrateConstructeurLinks extends AbstractMigration
|
|||||||
|
|
||||||
public function up(Schema $schema): void
|
public function up(Schema $schema): void
|
||||||
{
|
{
|
||||||
// Composant links (a = composantId, b = constructeurId)
|
// No-op: legacy M2M tables (_composantconstructeurs, _piececonstructeurs, etc.)
|
||||||
$this->addSql("
|
// were already dropped by Version20260331121257.
|
||||||
INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat)
|
// The actual data restoration is handled by Version20260405_RestoreConstructeurLinksFromBackup.
|
||||||
SELECT
|
|
||||||
'clmig_cc_' || ROW_NUMBER() OVER (ORDER BY a, b),
|
|
||||||
a, b, NULL, NOW(), NOW()
|
|
||||||
FROM _composantconstructeurs
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM composant_constructeur_links
|
|
||||||
WHERE composantid = _composantconstructeurs.a
|
|
||||||
AND constructeurid = _composantconstructeurs.b
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Piece links
|
|
||||||
$this->addSql("
|
|
||||||
INSERT INTO piece_constructeur_links (id, pieceid, constructeurid, supplierreference, createdat, updatedat)
|
|
||||||
SELECT
|
|
||||||
'clmig_pc_' || ROW_NUMBER() OVER (ORDER BY a, b),
|
|
||||||
a, b, NULL, NOW(), NOW()
|
|
||||||
FROM _piececonstructeurs
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM piece_constructeur_links
|
|
||||||
WHERE pieceid = _piececonstructeurs.a
|
|
||||||
AND constructeurid = _piececonstructeurs.b
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Machine links
|
|
||||||
$this->addSql("
|
|
||||||
INSERT INTO machine_constructeur_links (id, machineid, constructeurid, supplierreference, createdat, updatedat)
|
|
||||||
SELECT
|
|
||||||
'clmig_mc_' || ROW_NUMBER() OVER (ORDER BY a, b),
|
|
||||||
a, b, NULL, NOW(), NOW()
|
|
||||||
FROM _machineconstructeurs
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM machine_constructeur_links
|
|
||||||
WHERE machineid = _machineconstructeurs.a
|
|
||||||
AND constructeurid = _machineconstructeurs.b
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// Product links
|
|
||||||
$this->addSql("
|
|
||||||
INSERT INTO product_constructeur_links (id, productid, constructeurid, supplierreference, createdat, updatedat)
|
|
||||||
SELECT
|
|
||||||
'clmig_prc_' || ROW_NUMBER() OVER (ORDER BY a, b),
|
|
||||||
a, b, NULL, NOW(), NOW()
|
|
||||||
FROM _productconstructeurs
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM product_constructeur_links
|
|
||||||
WHERE productid = _productconstructeurs.a
|
|
||||||
AND constructeurid = _productconstructeurs.b
|
|
||||||
)
|
|
||||||
");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
// Remove only the migrated rows (identifiable by 'clmig_' prefix)
|
// No-op
|
||||||
$this->addSql("DELETE FROM composant_constructeur_links WHERE id LIKE 'clmig_cc_%'");
|
|
||||||
$this->addSql("DELETE FROM piece_constructeur_links WHERE id LIKE 'clmig_pc_%'");
|
|
||||||
$this->addSql("DELETE FROM machine_constructeur_links WHERE id LIKE 'clmig_mc_%'");
|
|
||||||
$this->addSql("DELETE FROM product_constructeur_links WHERE id LIKE 'clmig_prc_%'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ final class Version20260405_RestoreConstructeurLinksFromBackup extends AbstractM
|
|||||||
('clbkp3_cc_003', 'cmh0d59v5000347s561ahbept', 'cmhnaaoam000847s85wfwi2wm', NULL, NOW(), NOW()),
|
('clbkp3_cc_003', 'cmh0d59v5000347s561ahbept', 'cmhnaaoam000847s85wfwi2wm', NULL, NOW(), NOW()),
|
||||||
('clbkp3_cc_004', 'cmh0d59v5000347s561ahbept', 'cmg93n9sk000047uuwm6u20mj', NULL, NOW(), NOW()),
|
('clbkp3_cc_004', 'cmh0d59v5000347s561ahbept', 'cmg93n9sk000047uuwm6u20mj', NULL, NOW(), NOW()),
|
||||||
('clbkp3_cc_005', 'cmkr0nq1a004e1eq6v6ubxlfl', 'cmkqpnznr001p1eq6hdh2ept8', NULL, NOW(), NOW())
|
('clbkp3_cc_005', 'cmkr0nq1a004e1eq6v6ubxlfl', 'cmkqpnznr001p1eq6hdh2ept8', NULL, NOW(), NOW())
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
");
|
");
|
||||||
|
|
||||||
// === PIECE-CONSTRUCTEUR LINKS (25 from backup 3) ===
|
// === PIECE-CONSTRUCTEUR LINKS (25 from backup 3) ===
|
||||||
@@ -70,7 +70,7 @@ final class Version20260405_RestoreConstructeurLinksFromBackup extends AbstractM
|
|||||||
('clbkp3_pc_023', 'cl6480b97f6516fba22ce86434', 'cmg93n9sk000047uuwm6u20mj', NULL, NOW(), NOW()),
|
('clbkp3_pc_023', 'cl6480b97f6516fba22ce86434', 'cmg93n9sk000047uuwm6u20mj', NULL, NOW(), NOW()),
|
||||||
('clbkp3_pc_024', 'cl9579b05774d92096117841b0', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
('clbkp3_pc_024', 'cl9579b05774d92096117841b0', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
||||||
('clbkp3_pc_025', 'cl5b02c64fcc5ae8a3bfb6e5e6', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW())
|
('clbkp3_pc_025', 'cl5b02c64fcc5ae8a3bfb6e5e6', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW())
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
");
|
");
|
||||||
|
|
||||||
// Note: 6 additional Limatech piece links from backup (3) used old Limatech ID 'cla14aa4a50a799c2e54391be7'.
|
// Note: 6 additional Limatech piece links from backup (3) used old Limatech ID 'cla14aa4a50a799c2e54391be7'.
|
||||||
@@ -83,7 +83,7 @@ final class Version20260405_RestoreConstructeurLinksFromBackup extends AbstractM
|
|||||||
('clbkp3_pc_027', 'clfd3cb41a407ab5a3f9d5baae', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
('clbkp3_pc_027', 'clfd3cb41a407ab5a3f9d5baae', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
||||||
('clbkp3_pc_028', 'cld08dae22796b5855152580d9', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
('clbkp3_pc_028', 'cld08dae22796b5855152580d9', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW()),
|
||||||
('clbkp3_pc_029', 'clf6012fca41994c1e81ce2dba', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW())
|
('clbkp3_pc_029', 'clf6012fca41994c1e81ce2dba', 'cmizv4lm500071e2w6xymi2p6', NULL, NOW(), NOW())
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
");
|
");
|
||||||
|
|
||||||
// Re-enable FK checks
|
// Re-enable FK checks
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\ComposantConstructeurLink;
|
use App\Entity\ComposantConstructeurLink;
|
||||||
|
use App\Entity\Constructeur;
|
||||||
use App\Entity\MachineConstructeurLink;
|
use App\Entity\MachineConstructeurLink;
|
||||||
use App\Entity\PieceConstructeurLink;
|
use App\Entity\PieceConstructeurLink;
|
||||||
use App\Entity\ProductConstructeurLink;
|
use App\Entity\ProductConstructeurLink;
|
||||||
use App\Repository\ConstructeurRepository;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
@@ -19,7 +19,6 @@ final class ConstructeurStatsController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
private readonly ConstructeurRepository $constructeurs,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('/api/constructeurs/{id}/stats', name: 'api_constructeur_stats', methods: ['GET'])]
|
#[Route('/api/constructeurs/{id}/stats', name: 'api_constructeur_stats', methods: ['GET'])]
|
||||||
@@ -27,7 +26,7 @@ final class ConstructeurStatsController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
$constructeur = $this->constructeurs->find($id);
|
$constructeur = $this->em->find(Constructeur::class, $id);
|
||||||
if (!$constructeur) {
|
if (!$constructeur) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
['message' => 'Fournisseur introuvable.'],
|
['message' => 'Fournisseur introuvable.'],
|
||||||
@@ -35,7 +34,14 @@ final class ConstructeurStatsController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse($this->buildStats($id));
|
$bulk = $this->fetchAllStats();
|
||||||
|
|
||||||
|
return new JsonResponse($bulk[$id] ?? [
|
||||||
|
'composantCount' => 0,
|
||||||
|
'pieceCount' => 0,
|
||||||
|
'machineCount' => 0,
|
||||||
|
'productCount' => 0,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/constructeurs/stats', name: 'api_constructeurs_stats_bulk', methods: ['GET'], priority: 1)]
|
#[Route('/api/constructeurs/stats', name: 'api_constructeurs_stats_bulk', methods: ['GET'], priority: 1)]
|
||||||
@@ -43,42 +49,48 @@ final class ConstructeurStatsController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
$this->denyAccessUnlessGranted('ROLE_VIEWER');
|
||||||
|
|
||||||
|
return new JsonResponse($this->fetchAllStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{composantCount: int, pieceCount: int, machineCount: int, productCount: int}> */
|
||||||
|
private function fetchAllStats(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
// Initialize all constructeurs with zero counts
|
||||||
$allIds = $this->em->createQuery(
|
$allIds = $this->em->createQuery(
|
||||||
'SELECT c.id FROM App\Entity\Constructeur c',
|
'SELECT c.id FROM App\Entity\Constructeur c',
|
||||||
)->getSingleColumnResult();
|
)->getSingleColumnResult();
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($allIds as $id) {
|
foreach ($allIds as $id) {
|
||||||
$result[$id] = $this->buildStats($id);
|
$result[$id] = [
|
||||||
|
'composantCount' => 0,
|
||||||
|
'pieceCount' => 0,
|
||||||
|
'machineCount' => 0,
|
||||||
|
'productCount' => 0,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse($result);
|
// 4 bulk queries instead of 4N
|
||||||
}
|
$counts = [
|
||||||
|
'composantCount' => ComposantConstructeurLink::class,
|
||||||
/** @return array{composantCount: int, pieceCount: int, machineCount: int, productCount: int} */
|
'pieceCount' => PieceConstructeurLink::class,
|
||||||
private function buildStats(string $constructeurId): array
|
'machineCount' => MachineConstructeurLink::class,
|
||||||
{
|
'productCount' => ProductConstructeurLink::class,
|
||||||
$composantCount = (int) $this->em->createQuery(
|
|
||||||
'SELECT COUNT(l.id) FROM '.ComposantConstructeurLink::class.' l WHERE l.constructeur = :id',
|
|
||||||
)->setParameter('id', $constructeurId)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$pieceCount = (int) $this->em->createQuery(
|
|
||||||
'SELECT COUNT(l.id) FROM '.PieceConstructeurLink::class.' l WHERE l.constructeur = :id',
|
|
||||||
)->setParameter('id', $constructeurId)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$machineCount = (int) $this->em->createQuery(
|
|
||||||
'SELECT COUNT(l.id) FROM '.MachineConstructeurLink::class.' l WHERE l.constructeur = :id',
|
|
||||||
)->setParameter('id', $constructeurId)->getSingleScalarResult();
|
|
||||||
|
|
||||||
$productCount = (int) $this->em->createQuery(
|
|
||||||
'SELECT COUNT(l.id) FROM '.ProductConstructeurLink::class.' l WHERE l.constructeur = :id',
|
|
||||||
)->setParameter('id', $constructeurId)->getSingleScalarResult();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'composantCount' => $composantCount,
|
|
||||||
'pieceCount' => $pieceCount,
|
|
||||||
'machineCount' => $machineCount,
|
|
||||||
'productCount' => $productCount,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
foreach ($counts as $key => $entityClass) {
|
||||||
|
$rows = $this->em->createQuery(
|
||||||
|
'SELECT IDENTITY(l.constructeur) AS cid, COUNT(l.id) AS cnt FROM '.$entityClass.' l GROUP BY l.constructeur',
|
||||||
|
)->getResult();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (isset($result[$row['cid']])) {
|
||||||
|
$result[$row['cid']][$key] = (int) $row['cnt'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user