Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e105fd070 | ||
|
|
a0c4597de0 | ||
|
|
d3f269452c | ||
|
|
b3fa927e77 | ||
|
|
f71f4c68da | ||
|
|
905d5c0957 | ||
|
|
03a5d05a2c |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '1.9.33'
|
app.version: '1.9.37'
|
||||||
|
|||||||
@@ -69,9 +69,25 @@
|
|||||||
<span v-if="component.prix" class="text-[0.65rem] font-bold text-primary bg-primary/20 px-1.5 py-0.5 rounded border border-primary/30">{{ component.prix }}€</span>
|
<span v-if="component.prix" class="text-[0.65rem] font-bold text-primary bg-primary/20 px-1.5 py-0.5 rounded border border-primary/30">{{ component.prix }}€</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 1.5: Machine context fields (badges plus gros, visibles en lecture ET en edition) -->
|
||||||
|
<div
|
||||||
|
v-if="visibleContextFieldTags.length"
|
||||||
|
class="flex flex-wrap items-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="field in visibleContextFieldTags"
|
||||||
|
:key="field.name"
|
||||||
|
class="inline-flex items-baseline gap-1.5 px-2.5 py-1 rounded-md"
|
||||||
|
:class="contextFieldBadgeClass(field)"
|
||||||
|
>
|
||||||
|
<span class="text-[0.65rem] font-semibold uppercase tracking-wide opacity-70">{{ field.name }}</span>
|
||||||
|
<span class="text-sm font-bold">{{ field.value }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Row 2: Metadata tags -->
|
<!-- Row 2: Metadata tags -->
|
||||||
<div
|
<div
|
||||||
v-if="componentConstructeursDisplay.length || displayProductName || (!isEditMode && visibleContextFieldTags.length)"
|
v-if="componentConstructeursDisplay.length || displayProductName"
|
||||||
class="flex flex-wrap items-center gap-1.5"
|
class="flex flex-wrap items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -85,17 +101,6 @@
|
|||||||
<span v-if="displayProductName" class="text-[0.65rem] font-semibold text-info bg-info/20 px-1.5 py-0.5 rounded border border-info/30">
|
<span v-if="displayProductName" class="text-[0.65rem] font-semibold text-info bg-info/20 px-1.5 py-0.5 rounded border border-info/30">
|
||||||
{{ displayProductName }}
|
{{ displayProductName }}
|
||||||
</span>
|
</span>
|
||||||
<!-- Context field tags (consultation only) -->
|
|
||||||
<template v-if="!isEditMode">
|
|
||||||
<span
|
|
||||||
v-for="field in visibleContextFieldTags"
|
|
||||||
:key="field.name"
|
|
||||||
class="text-[0.65rem] font-semibold px-1.5 py-0.5 rounded"
|
|
||||||
:class="contextFieldBadgeClass(field)"
|
|
||||||
>
|
|
||||||
{{ field.name }} : {{ field.value }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,25 @@
|
|||||||
<span v-if="pieceData.prix" class="text-[0.65rem] font-bold text-primary bg-primary/20 px-1.5 py-0.5 rounded border border-primary/30">{{ pieceData.prix }}€</span>
|
<span v-if="pieceData.prix" class="text-[0.65rem] font-bold text-primary bg-primary/20 px-1.5 py-0.5 rounded border border-primary/30">{{ pieceData.prix }}€</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 1.5: Machine context fields (badges plus gros, visibles en lecture ET en edition) -->
|
||||||
|
<div
|
||||||
|
v-if="visibleContextFieldTags.length"
|
||||||
|
class="flex flex-wrap items-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="field in visibleContextFieldTags"
|
||||||
|
:key="field.name"
|
||||||
|
class="inline-flex items-baseline gap-1.5 px-2.5 py-1 rounded-md"
|
||||||
|
:class="contextFieldBadgeClass(field)"
|
||||||
|
>
|
||||||
|
<span class="text-[0.65rem] font-semibold uppercase tracking-wide opacity-70">{{ field.name }}</span>
|
||||||
|
<span class="text-sm font-bold">{{ field.value }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Row 2: Metadata tags -->
|
<!-- Row 2: Metadata tags -->
|
||||||
<div
|
<div
|
||||||
v-if="piece.parentComponentName || pieceConstructeursDisplay.length || displayProductName || (!isEditMode && visibleContextFieldTags.length)"
|
v-if="piece.parentComponentName || pieceConstructeursDisplay.length || displayProductName"
|
||||||
class="flex flex-wrap items-center gap-1.5"
|
class="flex flex-wrap items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<span v-if="piece.parentComponentName" class="text-[0.65rem] text-base-content/40 bg-base-300/20 px-1.5 py-0.5 rounded">
|
<span v-if="piece.parentComponentName" class="text-[0.65rem] text-base-content/40 bg-base-300/20 px-1.5 py-0.5 rounded">
|
||||||
@@ -90,17 +106,6 @@
|
|||||||
<span v-if="displayProductName" class="text-[0.65rem] font-semibold text-info bg-info/20 px-1.5 py-0.5 rounded border border-info/30">
|
<span v-if="displayProductName" class="text-[0.65rem] font-semibold text-info bg-info/20 px-1.5 py-0.5 rounded border border-info/30">
|
||||||
{{ displayProductName }}
|
{{ displayProductName }}
|
||||||
</span>
|
</span>
|
||||||
<!-- Context field tags (consultation only) -->
|
|
||||||
<template v-if="!isEditMode">
|
|
||||||
<span
|
|
||||||
v-for="field in visibleContextFieldTags"
|
|
||||||
:key="field.name"
|
|
||||||
class="text-[0.65rem] font-semibold px-1.5 py-0.5 rounded"
|
|
||||||
:class="contextFieldBadgeClass(field)"
|
|
||||||
>
|
|
||||||
{{ field.name }} : {{ field.value }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
import { extractCollection, extractTotal } from '~/shared/utils/apiHelpers'
|
||||||
|
|
||||||
export interface ConstructeurTelephone {
|
export interface ConstructeurTelephone {
|
||||||
'@id'?: string
|
'@id'?: string
|
||||||
@@ -33,6 +33,24 @@ interface ConstructeurResult {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConstructeurPageOptions {
|
||||||
|
page?: number
|
||||||
|
itemsPerPage?: number
|
||||||
|
search?: string
|
||||||
|
categoryId?: string
|
||||||
|
orderField?: 'name' | 'email' | 'createdAt'
|
||||||
|
orderDirection?: 'asc' | 'desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConstructeurPageResult {
|
||||||
|
success: boolean
|
||||||
|
items: Constructeur[]
|
||||||
|
totalItems: number
|
||||||
|
totalPages: number
|
||||||
|
currentPage: number
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
const constructeurs = ref<Constructeur[]>([])
|
const constructeurs = ref<Constructeur[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
@@ -83,8 +101,10 @@ export function useConstructeurs() {
|
|||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
const params = new URLSearchParams()
|
||||||
const result = await get(`/constructeurs${query}`)
|
params.set('itemsPerPage', '2000')
|
||||||
|
if (search) params.set('search', search)
|
||||||
|
const result = await get(`/constructeurs?${params.toString()}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
constructeurs.value = uniqueConstructeurs(items)
|
constructeurs.value = uniqueConstructeurs(items)
|
||||||
@@ -104,6 +124,37 @@ export function useConstructeurs() {
|
|||||||
return loadConstructeurs(search)
|
return loadConstructeurs(search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchConstructeursPage = async (opts: ConstructeurPageOptions = {}): Promise<ConstructeurPageResult> => {
|
||||||
|
const page = Math.max(1, opts.page ?? 1)
|
||||||
|
const itemsPerPage = Math.max(1, opts.itemsPerPage ?? 30)
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('page', String(page))
|
||||||
|
params.set('itemsPerPage', String(itemsPerPage))
|
||||||
|
if (opts.search && opts.search.trim()) params.set('search', opts.search.trim())
|
||||||
|
if (opts.categoryId) params.set('categories.id', opts.categoryId)
|
||||||
|
if (opts.orderField) {
|
||||||
|
params.set(`order[${opts.orderField}]`, opts.orderDirection ?? 'asc')
|
||||||
|
}
|
||||||
|
const result = await get(`/constructeurs?${params.toString()}`)
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, items: [], totalItems: 0, totalPages: 0, currentPage: page, error: result.error }
|
||||||
|
}
|
||||||
|
const items = extractCollection<Constructeur>(result.data)
|
||||||
|
const totalItems = extractTotal(result.data, items.length)
|
||||||
|
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage))
|
||||||
|
upsertConstructeurs(items)
|
||||||
|
return { success: true, items, totalItems, totalPages, currentPage: page }
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
|
console.error('Erreur lors du chargement de la page fournisseurs:', error)
|
||||||
|
return { success: false, items: [], totalItems: 0, totalPages: 0, currentPage: page, error: err.message }
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createConstructeur = async (data: Record<string, unknown>): Promise<ConstructeurResult> => {
|
const createConstructeur = async (data: Record<string, unknown>): Promise<ConstructeurResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -227,6 +278,7 @@ export function useConstructeurs() {
|
|||||||
loading,
|
loading,
|
||||||
loadConstructeurs,
|
loadConstructeurs,
|
||||||
searchConstructeurs,
|
searchConstructeurs,
|
||||||
|
fetchConstructeursPage,
|
||||||
createConstructeur,
|
createConstructeur,
|
||||||
updateConstructeur,
|
updateConstructeur,
|
||||||
deleteConstructeur,
|
deleteConstructeur,
|
||||||
|
|||||||
@@ -19,13 +19,17 @@
|
|||||||
<div class="card-body space-y-4">
|
<div class="card-body space-y-4">
|
||||||
<DataTable
|
<DataTable
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="filteredConstructeurs"
|
:rows="pageItems"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:sort="currentSort"
|
:sort="currentSort"
|
||||||
:show-counter="false"
|
:pagination="paginationState"
|
||||||
|
:show-counter="true"
|
||||||
|
:show-per-page="true"
|
||||||
empty-message="Aucun fournisseur trouvé."
|
empty-message="Aucun fournisseur trouvé."
|
||||||
no-results-message="Aucun fournisseur trouvé."
|
no-results-message="Aucun fournisseur trouvé."
|
||||||
@sort="handleSort"
|
@sort="handleSort"
|
||||||
|
@update:current-page="onPageChange"
|
||||||
|
@update:per-page="onPerPageChange"
|
||||||
>
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="flex flex-col sm:flex-row gap-3 w-full">
|
<div class="flex flex-col sm:flex-row gap-3 w-full">
|
||||||
@@ -204,7 +208,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } 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'
|
||||||
import FieldPhone from '~/components/form/FieldPhone.vue'
|
import FieldPhone from '~/components/form/FieldPhone.vue'
|
||||||
@@ -229,10 +233,17 @@ interface ConstructeurFormState {
|
|||||||
|
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const { canEdit } = usePermissions()
|
const { canEdit } = usePermissions()
|
||||||
const { constructeurs, loading, searchConstructeurs, createConstructeur, updateConstructeur, deleteConstructeur, loadConstructeurs } = useConstructeurs()
|
const { constructeurs, loading, createConstructeur, updateConstructeur, deleteConstructeur, fetchConstructeursPage } = useConstructeurs()
|
||||||
const { categories: allCategories, loadCategories } = useConstructeurCategories()
|
const { categories: allCategories, loadCategories } = useConstructeurCategories()
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
|
|
||||||
|
const pageItems = ref<typeof constructeurs.value>([])
|
||||||
|
const totalItems = ref(0)
|
||||||
|
const totalPages = ref(0)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const perPage = ref(30)
|
||||||
|
const perPageOptions = [15, 30, 50, 100]
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'name', label: 'Nom', sortable: true },
|
{ key: 'name', label: 'Nom', sortable: true },
|
||||||
{ key: 'email', label: 'Email', sortable: true },
|
{ key: 'email', label: 'Email', sortable: true },
|
||||||
@@ -261,6 +272,44 @@ const handleSort = (sort) => {
|
|||||||
sortDir.value = sort.direction
|
sortDir.value = sort.direction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paginationState = computed(() => ({
|
||||||
|
currentPage: currentPage.value,
|
||||||
|
totalPages: totalPages.value,
|
||||||
|
totalItems: totalItems.value,
|
||||||
|
pageItems: pageItems.value.length,
|
||||||
|
perPage: perPage.value,
|
||||||
|
perPageOptions,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const SORTABLE_FIELDS = new Set(['name', 'email', 'createdAt'])
|
||||||
|
|
||||||
|
const loadPage = async () => {
|
||||||
|
const orderField = SORTABLE_FIELDS.has(sortKey.value)
|
||||||
|
? (sortKey.value as 'name' | 'email' | 'createdAt')
|
||||||
|
: 'name'
|
||||||
|
const result = await fetchConstructeursPage({
|
||||||
|
page: currentPage.value,
|
||||||
|
itemsPerPage: perPage.value,
|
||||||
|
search: searchTerm.value,
|
||||||
|
categoryId: selectedCategoryId.value || undefined,
|
||||||
|
orderField,
|
||||||
|
orderDirection: sortDir.value === 'desc' ? 'desc' : 'asc',
|
||||||
|
})
|
||||||
|
if (!result.success) {
|
||||||
|
if (result.error) showError(result.error)
|
||||||
|
pageItems.value = []
|
||||||
|
totalItems.value = 0
|
||||||
|
totalPages.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pageItems.value = result.items
|
||||||
|
totalItems.value = result.totalItems
|
||||||
|
totalPages.value = result.totalPages
|
||||||
|
if (currentPage.value > result.totalPages && result.totalPages > 0) {
|
||||||
|
currentPage.value = result.totalPages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const modalOpen = ref(false)
|
const modalOpen = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const editingConstructeur = ref<Record<string, any> | null>(null)
|
const editingConstructeur = ref<Record<string, any> | null>(null)
|
||||||
@@ -268,29 +317,31 @@ const form = ref<ConstructeurFormState>({ name: '', email: '', telephones: [], c
|
|||||||
|
|
||||||
const rowPhones = constructeurPhones
|
const rowPhones = constructeurPhones
|
||||||
|
|
||||||
const filteredConstructeurs = computed(() => {
|
const debouncedSearch = debounce(() => {
|
||||||
const key = sortKey.value
|
currentPage.value = 1
|
||||||
const dir = sortDir.value === 'desc' ? -1 : 1
|
loadPage()
|
||||||
let sorted = [...constructeurs.value].sort((a, b) => {
|
}, 300)
|
||||||
if (key === 'createdAt') {
|
|
||||||
return dir * (new Date(a[key] || 0).getTime() - new Date(b[key] || 0).getTime())
|
watch(selectedCategoryId, () => {
|
||||||
}
|
currentPage.value = 1
|
||||||
return dir * (a[key] || '').localeCompare(b[key] || '')
|
loadPage()
|
||||||
})
|
|
||||||
if (selectedCategoryId.value) {
|
|
||||||
sorted = sorted.filter(item => (item.categories || []).some(cat => cat.id === selectedCategoryId.value))
|
|
||||||
}
|
|
||||||
if (!searchTerm.value) { return sorted }
|
|
||||||
const term = searchTerm.value.toLowerCase()
|
|
||||||
return sorted.filter((item) => {
|
|
||||||
const haystack = [item.name, item.email, ...rowPhones(item).map(t => t.numero)]
|
|
||||||
return haystack.some(value => value && String(value).toLowerCase().includes(term))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const debouncedSearch = debounce(async () => {
|
watch([sortKey, sortDir], () => {
|
||||||
await searchConstructeurs(searchTerm.value)
|
currentPage.value = 1
|
||||||
}, 300)
|
loadPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onPageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
loadPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPerPageChange = (value: number) => {
|
||||||
|
perPage.value = value
|
||||||
|
currentPage.value = 1
|
||||||
|
loadPage()
|
||||||
|
}
|
||||||
|
|
||||||
const formatDate = formatFrenchDate
|
const formatDate = formatFrenchDate
|
||||||
|
|
||||||
@@ -386,7 +437,7 @@ const saveConstructeur = async () => {
|
|||||||
saving.value = false
|
saving.value = false
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
closeModal()
|
closeModal()
|
||||||
await searchConstructeurs(searchTerm.value)
|
await loadPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +448,10 @@ const confirmDelete = async (constructeur) => {
|
|||||||
const result = await deleteConstructeur(constructeur.id)
|
const result = await deleteConstructeur(constructeur.id)
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
await loadPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +463,7 @@ const loadStats = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadConstructeurs()
|
loadPage()
|
||||||
loadCategories()
|
loadCategories()
|
||||||
loadStats()
|
loadStats()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
use ApiPlatform\Metadata\Delete;
|
use ApiPlatform\Metadata\Delete;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
@@ -12,6 +15,7 @@ use ApiPlatform\Metadata\Patch;
|
|||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\Metadata\Put;
|
use ApiPlatform\Metadata\Put;
|
||||||
use App\Entity\Trait\CuidEntityTrait;
|
use App\Entity\Trait\CuidEntityTrait;
|
||||||
|
use App\Filter\ConstructeurSearchFilter;
|
||||||
use App\Repository\ConstructeurRepository;
|
use App\Repository\ConstructeurRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -26,6 +30,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
|
||||||
#[ORM\Table(name: 'constructeurs')]
|
#[ORM\Table(name: 'constructeurs')]
|
||||||
#[ORM\HasLifecycleCallbacks]
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ApiFilter(ConstructeurSearchFilter::class)]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['categories.id' => 'exact'])]
|
||||||
|
#[ApiFilter(OrderFilter::class, properties: ['name', 'email', 'createdAt'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
description: 'Fournisseurs et constructeurs. Référentiel partagé entre les machines, pièces, composants et produits pour identifier les fabricants et distributeurs.',
|
description: 'Fournisseurs et constructeurs. Référentiel partagé entre les machines, pièces, composants et produits pour identifier les fabricants et distributeurs.',
|
||||||
operations: [
|
operations: [
|
||||||
@@ -37,7 +44,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
|
||||||
],
|
],
|
||||||
paginationClientItemsPerPage: true,
|
paginationClientItemsPerPage: true,
|
||||||
paginationMaximumItemsPerPage: 200,
|
paginationMaximumItemsPerPage: 2000,
|
||||||
normalizationContext: ['groups' => ['constructeur:read']],
|
normalizationContext: ['groups' => ['constructeur:read']],
|
||||||
denormalizationContext: ['groups' => ['constructeur:write']]
|
denormalizationContext: ['groups' => ['constructeur:write']]
|
||||||
)]
|
)]
|
||||||
|
|||||||
65
src/Filter/ConstructeurSearchFilter.php
Normal file
65
src/Filter/ConstructeurSearchFilter.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filter;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
|
||||||
|
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use App\Entity\ConstructeurTelephone;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search filter pour Constructeur : LIKE insensible à la casse sur name, email
|
||||||
|
* + LEFT JOIN sur la collection telephones pour matcher aussi sur telephone.numero.
|
||||||
|
* Param query : ?search=...
|
||||||
|
*/
|
||||||
|
final class ConstructeurSearchFilter extends AbstractFilter
|
||||||
|
{
|
||||||
|
public function getDescription(string $resourceClass): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'search' => [
|
||||||
|
'property' => null,
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => false,
|
||||||
|
'description' => 'Recherche dans le nom, l\'email et les numéros de téléphone du fournisseur.',
|
||||||
|
'openapi' => [
|
||||||
|
'allowEmptyValue' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
|
||||||
|
{
|
||||||
|
if ('search' !== $property || !is_string($value) || '' === trim($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$alias = $queryBuilder->getRootAliases()[0];
|
||||||
|
$telAlias = $queryNameGenerator->generateJoinAlias('phoneSearch');
|
||||||
|
$paramName = $queryNameGenerator->generateParameterName('search');
|
||||||
|
$likePattern = '%'.mb_strtolower(trim($value)).'%';
|
||||||
|
|
||||||
|
$em = $queryBuilder->getEntityManager();
|
||||||
|
$phoneSubQuery = $em->createQueryBuilder()
|
||||||
|
->select('1')
|
||||||
|
->from(ConstructeurTelephone::class, $telAlias)
|
||||||
|
->where(sprintf('%1$s.constructeur = %2$s', $telAlias, $alias))
|
||||||
|
->andWhere(sprintf('LOWER(%s.numero) LIKE :%s', $telAlias, $paramName))
|
||||||
|
->getDQL()
|
||||||
|
;
|
||||||
|
|
||||||
|
$queryBuilder
|
||||||
|
->andWhere(sprintf(
|
||||||
|
'LOWER(%1$s.name) LIKE :%2$s OR LOWER(%1$s.email) LIKE :%2$s OR EXISTS (%3$s)',
|
||||||
|
$alias,
|
||||||
|
$paramName,
|
||||||
|
$phoneSubQuery,
|
||||||
|
))
|
||||||
|
->setParameter($paramName, $likePattern)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user