[#FER-30] Revoir l'affichage type bovin #57

Merged
tristan merged 2 commits from feature/FER-30-revoir-l-affichage-type-bovin into develop 2026-05-21 09:34:41 +00:00
9 changed files with 109 additions and 20 deletions

View File

@@ -69,6 +69,7 @@ Ajouter dans le fichier .env du frontend
* [#FER-27] Fix export inventaire bovin * [#FER-27] Fix export inventaire bovin
* [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin * [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin
* [#FER-22] Pouvoir exporter les réceptions/expéditions fines en Excel * [#FER-22] Pouvoir exporter les réceptions/expéditions fines en Excel
* [#FER-30] Revoir l'affichage type bovin
### Changed ### Changed

View File

@@ -5,12 +5,10 @@
@submit.prevent="goNext" @submit.prevent="goNext"
> >
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1> <h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
<div <div class="grid grid-cols-4 gap-x-8 gap-y-6">
class="flex flex-row gap-8 items-center w-full">
<div <div
v-for="type in bovineType" v-for="type in bovineType"
:key="type.id" :key="type.id">
class="mt-8 flex flex-row mb-2 w-full">
<UiNumberInput <UiNumberInput
:id="type.id" :id="type.id"
:label="type.label" :label="type.label"
@@ -23,12 +21,11 @@
wrapper-class="gap-3" wrapper-class="gap-3"
/> />
</div> </div>
<div <div>
class="mt-8 flex flex-row mb-2 gap-6">
<UiNumberInput <UiNumberInput
label="Autres" label="Autres"
v-model="otherQuantity" v-model="otherQuantity"
class="max-w-[80px]" class="max-w-[150px]"
wrapper-class="gap-3" wrapper-class="gap-3"
/> />
</div> </div>
@@ -79,7 +76,7 @@ const totalBovines = computed(() => {
const loadBovineType = async () => { const loadBovineType = async () => {
isLoadingBovineType.value = true isLoadingBovineType.value = true
try { try {
bovineType.value = await getBovineTypeList() bovineType.value = await getBovineTypeList({ display: true })
} finally { } finally {
isLoadingBovineType.value = false isLoadingBovineType.value = false
} }

View File

@@ -29,7 +29,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { getBovineTypeList } from '~/services/bovine-type' import { getBovineTypeList } from '~/services/bovine-type'
import type { BovineTypeData } from '~/services/dto/bovine-type-data' import type { BovineTypeData } from '~/services/dto/bovine-type-data'
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data' import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
@@ -45,7 +45,18 @@ const emit = defineEmits<{
(event: 'update:otherQuantity', value: number | null): void (event: 'update:otherQuantity', value: number | null): void
}>() }>()
const bovineTypes = ref<BovineTypeData[]>([]) // Types activés par l'admin (display=true), chargés depuis l'API.
const displayedTypes = ref<BovineTypeData[]>([])
// On affiche les types activés ET ceux déjà saisis sur la réception (même masqués),
// pour ne pas faire disparaître/perdre une quantité existante.
const bovineTypes = computed<BovineTypeData[]>(() => {
const seen = new Set(displayedTypes.value.map((type) => type.id))
const fromExisting = props.modelValue
.map((entry) => entry.bovineType)
.filter((type): type is BovineTypeData => Boolean(type) && !seen.has(type.id))
return [...displayedTypes.value, ...fromExisting]
})
const localQuantities = reactive<Record<string, number | null>>({}) const localQuantities = reactive<Record<string, number | null>>({})
const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0) const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
// Verrou pour éviter les boucles props -> local -> emit -> props. // Verrou pour éviter les boucles props -> local -> emit -> props.
@@ -154,8 +165,13 @@ watch(
{ deep: true } { deep: true }
) )
// Re-synchronise dès que la liste fusionnée change (chargement async des types).
watch(bovineTypes, () => {
syncLocalFromProps()
})
onMounted(async () => { onMounted(async () => {
bovineTypes.value = await getBovineTypeList() displayedTypes.value = await getBovineTypeList({ display: true })
syncLocalFromProps() syncLocalFromProps()
}) })
</script> </script>

View File

@@ -14,10 +14,13 @@
</h1> </h1>
</div> </div>
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]"> <div class="grid grid-cols-2 items-start pt-7 mb-8 gap-x-[200px]">
<UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" required /> <UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" required />
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" required /> <UiTextInput label="Code bovin" id="code-id" v-model="form.code" required />
</div> </div>
<div class="mb-11">
<UiCheckbox v-model="form.display" label="Afficher dans les réceptions" />
</div>
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<UiButton <UiButton
type="submit" type="submit"
@@ -53,7 +56,8 @@ function resolveId(param: unknown) {
const form = reactive<BovinFormData>({ const form = reactive<BovinFormData>({
label: '', label: '',
code: '' code: '',
display: false
}) })
@@ -64,6 +68,7 @@ const hydrateFromBovin = (bovin: BovineTypeData | null) => {
isHydrating.value = true isHydrating.value = true
form.label = bovin.label ?? '' form.label = bovin.label ?? ''
form.code = bovin.code ?? '' form.code = bovin.code ?? ''
form.display = bovin.display ?? false
isHydrating.value = false isHydrating.value = false
} }
@@ -92,8 +97,8 @@ async function validate() {
const basePayload = { const basePayload = {
label: normalizedBovinLabel, label: normalizedBovinLabel,
code: normalizedBovinCode code: normalizedBovinCode,
display: form.display
} }
isLoading.value = true isLoading.value = true

View File

@@ -29,6 +29,14 @@
<template #header-code> <template #header-code>
<UiTextInput v-model="filters.code" placeholder="Code" size="compact" /> <UiTextInput v-model="filters.code" placeholder="Code" size="compact" />
</template> </template>
<template #cell-display="{ item }">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-sm font-medium"
:class="item.display ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'"
>
{{ item.display ? 'Oui' : 'Non' }}
</span>
</template>
</UiDataTable> </UiDataTable>
</div> </div>
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400"> <div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
@@ -58,7 +66,8 @@ const { items, totalItems, page, perPage, filters, loading, reload } =
const columns = [ const columns = [
{ key: 'label', label: 'Nom' }, { key: 'label', label: 'Nom' },
{ key: 'code', label: 'Code' } { key: 'code', label: 'Code' },
{ key: 'display', label: 'Affiché en réception' }
] ]
const goToBovin = (bovin: BovineTypeData) => { const goToBovin = (bovin: BovineTypeData) => {

View File

@@ -5,9 +5,13 @@ export type BovineTypeListResponse =
| BovineTypeData[] | BovineTypeData[]
| { 'hydra:member'?: BovineTypeData[] } | { 'hydra:member'?: BovineTypeData[] }
export async function getBovineTypeList(): Promise<BovineTypeData[]> { export async function getBovineTypeList(filters: { display?: boolean } = {}): Promise<BovineTypeData[]> {
const api = useApi() const api = useApi()
const response = await api.get<BovineTypeListResponse>('bovine_types', {}, { const query: Record<string, string> = {}
if (filters.display !== undefined) {
query.display = filters.display ? 'true' : 'false'
}
const response = await api.get<BovineTypeListResponse>('bovine_types', query, {
toastErrorKey: 'errors.bovin.list' toastErrorKey: 'errors.bovin.list'
}) })
@@ -51,10 +55,12 @@ export async function updateBovin(id: number, payload: BovinPayload = {}): Promi
const mapToBovineTypeData = (item: BovineTypeData): BovineTypeData => ({ const mapToBovineTypeData = (item: BovineTypeData): BovineTypeData => ({
id: item.id, id: item.id,
label: item.label, label: item.label,
code: item.code code: item.code,
display: item.display ?? false
}) })
const toBovineTypePayload = (payload: BovinPayload): Partial<BovineTypeData> => ({ const toBovineTypePayload = (payload: BovinPayload): Partial<BovineTypeData> => ({
label: payload.label ?? undefined, label: payload.label ?? undefined,
code: payload.code ?? undefined code: payload.code ?? undefined,
display: payload.display ?? undefined
}) })

View File

@@ -2,14 +2,17 @@ export interface BovineTypeData{
id: number id: number
label: string label: string
code: string code: string
display: boolean
} }
export interface BovinFormData { export interface BovinFormData {
label: string label: string
code: string code: string
display: boolean
} }
export type BovinPayload = { export type BovinPayload = {
label?: string | null label?: string | null
code?: string | null code?: string | null
display?: boolean | null
} }

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260521092455 extends AbstractMigration
{
public function getDescription(): string
{
return 'Ajoute la colonne display sur bovine_type (défaut false) pour piloter l\'affichage dans une réception';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE bovine_type ADD display BOOLEAN DEFAULT false NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE bovine_type DROP display');
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
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;
@@ -19,6 +20,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
'label' => 'ipartial', 'label' => 'ipartial',
'code' => 'ipartial', 'code' => 'ipartial',
])] ])]
#[ApiFilter(BooleanFilter::class, properties: ['display'])]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get( new Get(
@@ -58,6 +60,15 @@ class BovineType
#[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read', 'bovine:read', 'building_case:read'])] #[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read', 'bovine:read', 'building_case:read'])]
private ?string $code = null; private ?string $code = null;
/**
* Détermine si le type bovin est proposé à la sélection lors d'une réception.
* Les types créés automatiquement par la synchro inventaire arrivent à false ;
* seul un admin peut les activer.
*/
#[ORM\Column(options: ['default' => false])]
#[Groups(['bovine-type:read', 'bovine-type:write'])]
private bool $display = false;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@@ -86,4 +97,16 @@ class BovineType
return $this; return $this;
} }
public function isDisplay(): bool
{
return $this->display;
}
public function setDisplay(bool $display): static
{
$this->display = $display;
return $this;
}
} }