[#FER-30] Revoir l'affichage type bovin #57
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
29
migrations/Version20260521092455.php
Normal file
29
migrations/Version20260521092455.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user