[#256] Créer une nouvelle réception (étape 3 - bovin) #11

Merged
tristan merged 6 commits from feat/256-reception-etape-3-bovin into develop 2026-02-05 09:29:29 +00:00
10 changed files with 230 additions and 76 deletions
Showing only changes of commit 81c2a5802b - Show all commits

68
.idea/workspace.xml generated
View File

@@ -5,16 +5,16 @@
</component>
<component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
<change afterPath="$PROJECT_DIR$/frontend/services/dto/reception-bovine-data.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/frontend/services/reception-bovine.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-bovine-received.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-bovine-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/reception-bovine.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception-bovine.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/BovineType.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/BovineType.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/ReceptionBovine.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/ReceptionBovine.php" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -226,36 +226,36 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feat/256-reception-etape-3-bovin&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/sroy/Documents/test/Ferme&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.MCP Project settings loaded": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "feat/256-reception-etape-3-bovin",
"last_opened_file_path": "/home/sroy/Documents/test/Ferme",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "configurable.tailwindcss",
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
"vue.rearranger.settings.migration": "true"
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
],
&quot;com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File&quot;: [
&quot;TEXT&quot;
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
"TEXT"
],
&quot;vue.recent.templates&quot;: [
&quot;Vue Composition API Component&quot;
"vue.recent.templates": [
"Vue Composition API Component"
]
}
}</component>
}]]></component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
@@ -300,7 +300,7 @@
<workItem from="1770102495553" duration="2280000" />
<workItem from="1770195604082" duration="90000" />
<workItem from="1770195718952" duration="215000" />
<workItem from="1770195959162" duration="8094000" />
<workItem from="1770195959162" duration="18482000" />
</task>
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
<option name="closed" value="true" />
@@ -678,7 +678,15 @@
<option name="project" value="LOCAL" />
<updated>1770131226364</updated>
</task>
<option name="localTasksCounter" value="48" />
<task id="LOCAL-00048" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
<option name="closed" value="true" />
<created>1770206668867</created>
<option name="number" value="00048" />
<option name="presentableId" value="LOCAL-00048" />
<option name="project" value="LOCAL" />
<updated>1770206668867</updated>
</task>
<option name="localTasksCounter" value="49" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">

View File

@@ -4,7 +4,7 @@
class="flex flex-col items-center gap-16">
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
<div
class="flex flex-row gap-16 items-center w-full">
class="flex flex-row gap-8 items-center">
<div
v-for="type in bovineType"
:key="type.id"
@@ -12,14 +12,17 @@
<UiNumberInput
:label="type.label"
:code="type.code"
v-model="selectedBovineTypeIds[type.id]"
:value="String(type.id)"
v-model="bovineQuantities[String(type.id)]"
:placeholder="0"
:min="0"
:max="10"
/>
</div>
<div
class="mt-8 flex flex-row mb-2 gap-6">
<UiNumberInput
label="Autres"
v-model="otherQuantity"
/>
</div>
</div>
@@ -38,14 +41,20 @@ import {useReceptionStore} from '~/stores/reception'
import {
createReceptionBovine,
deleteReceptionBovine,
getReceptionBovineList
getReceptionBovineList,
updateReceptionBovine
} from "~/services/reception-bovine";
import {ref} from "vue";
import {computed, onMounted, reactive, ref, watch} from "vue";
const isLoadingBovineType = ref(false)
const bovineType = ref<BovineTypeData[]>([])
const receptionStore = useReceptionStore()
const selectedBovineTypeIds = ref<string[]>
const bovineQuantities = reactive<Record<string, number | null>>({})
const otherQuantity = ref<number | null>(0)
const receptionId = computed(() => receptionStore.current?.id ?? null)
const receptionIri = computed(() =>
receptionId.value ? `/api/receptions/${receptionId.value}` : null
)
const loadBovineType = async () => {
isLoadingBovineType.value = true
try {
@@ -55,28 +64,94 @@ const loadBovineType = async () => {
}
}
onMounted(async () => {
await loadBovineType()
selectedBovineTypeIds.value=bovineType.value.map((type) => String(type.id))
})
watch(
() => receptionId.value,
async (id) => {
if (!id || !receptionIri.value) {
return
}
const selectionMap: Record<string, number | null> = {}
for (const type of bovineType.value) {
selectionMap[String(type.id)] = 0
}
const existing = await getReceptionBovineList(receptionIri.value)
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
selectionMap[bovineTypeId] = selection.quantity ?? 0
}
for (const key of Object.keys(bovineQuantities)) {
delete bovineQuantities[key]
}
Object.assign(bovineQuantities, selectionMap)
},
{immediate: true}
)
async function syncBovineSelections(receptionIri: string) {
const existing = await getReceptionBovineList(receptionIri)
const existingMap = new Map<string, { id: number; quantity: number | null }>()
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
existingMap.set(bovineTypeId, {
id: selection.id,
quantity: selection.quantity ?? 0
})
}
// Supprime les entrées supprimées ou modifiées
for (const [bovineTypeId, entry] of existingMap.entries()) {
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
if (!selectedQuantity) {
await deleteReceptionBovine(entry.id)
existingMap.delete(bovineTypeId)
continue
}
if (selectedQuantity !== entry.quantity) {
await updateReceptionBovine(entry.id, { quantity: selectedQuantity })
existingMap.set(bovineTypeId, {
id: entry.id,
quantity: selectedQuantity
})
}
}
// Crée les entrées manquantes
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
if (!quantity) {
continue
}
if (existingMap.has(bovineTypeId)) {
// Déjà à jour
continue
}
await createReceptionBovine({
reception: receptionIri,
bovineType: `/api/bovine_types/${bovineTypeId}`,
quantity
})
}
}
async function goNext() {
if (!receptionStore.current) {
if (!receptionStore.current || !receptionIri.value) {
return
}
const nextStep = receptionStore.current.currentStep + 1
const receptionIri = `/api/receptions/${receptionStore.current.id}`
console.log(selectedBovineTypeIds.value)
await syncBovineSelections(receptionIri.value)
await receptionStore.updateReception(receptionStore.current.id, {
merchandiseType: null,
merchandiseDetail: null,
buildings: null,
bovinesTypes : selectedBovineTypeIds.value.map((id) => `/api/bovines_types/${id}`),
currentStep: nextStep
})
}
</script>

View File

@@ -175,7 +175,6 @@ onMounted(async () => {
}
selectedPelletBuildingIds.value = selectionMap
})
// Enregistre les sélections et passe à l'étape suivante
async function goNext() {
if (!receptionStore.current) {
@@ -193,6 +192,7 @@ async function goNext() {
buildings: isGranule.value
? []
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
bovinesTypes: null,
currentStep: nextStep
})
@@ -210,7 +210,6 @@ async function clearPelletSelections(receptionIri: string) {
await deleteReceptionPelletBuilding(selection.id)
}
}
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
async function syncPelletSelections(receptionIri: string) {
const existing = await getReceptionPelletBuildingList(receptionIri)

View File

@@ -1,12 +1,19 @@
<template>
<div :class="['flex items-center gap-4', wrapperClass]">
<div :class="['flex flex-row items-center gap-2', wrapperClass]">
<label
v-if="label"
:for="id"
class="font-bold uppercase text-xl"
class="text-xl text-bold flex items-center gap-2"
:class="labelClass"
>
<span
v-if="label">
{{ label }}
</span>
<span
v-if="code" class="text-neutral-600">
({{ code }})
</span>
</label>
<input
:id="id"
@@ -17,7 +24,7 @@
:step="step"
:disabled="disabled"
v-bind="attrs"
class="border-b border-black text-xl pb-[6px] bg-transparent text-right"
class="border-b border-black text-xl bg-transparent w-48"
:class="[
isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-text',
@@ -29,14 +36,15 @@
</template>
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
import {computed, useAttrs} from 'vue'
defineOptions({ inheritAttrs: false })
defineOptions({inheritAttrs: false})
const props = withDefaults(
defineProps<{
id?: string
label?: string
code?: string
modelValue: number | string | null | undefined
min?: number | string
max?: number | string

View File

@@ -16,8 +16,12 @@
</div>
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
<ReceptionProductReceived v-if="storeReception?.currentStep === 2 && receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
<ReceptionBovineReceived v-if="storeReception?.currentStep === 2 && receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
<ReceptionProductReceived
v-if="storeReception?.currentStep === 2 &&
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
<ReceptionBovineReceived
v-if="storeReception?.currentStep === 2 &&
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
</div>
</template>

View File

@@ -47,3 +47,13 @@ export async function deleteReceptionBovine(id: number): Promise<void> {
toastErrorKey: 'errors.receptionBovine.delete'
})
}
export async function updateReceptionBovine(
id: number,
payload: Partial<ReceptionBovinePayload>
): Promise<ReceptionBovineTypeData> {
const api = useApi()
return api.patch<ReceptionBovineTypeData>(`reception_bovines/${id}`, payload, {
toastErrorKey: 'errors.receptionBovine.update'
})
}

View File

@@ -0,0 +1,33 @@
<?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 Version20260204141406 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE reception_bovine ALTER quantity SET DEFAULT 0');
$this->addSql('CREATE UNIQUE INDEX uniq_reception_bovine_type ON reception_bovine (reception_id, bovine_type_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX uniq_reception_bovine_type');
$this->addSql('ALTER TABLE reception_bovine ALTER quantity DROP DEFAULT');
}
}

View File

@@ -28,15 +28,15 @@ class BovineType
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['bovine-type:read', 'reception:read'])]
#[Groups(['bovine-type:read', 'reception:read', 'reception-bovine:read'])]
private ?int $id = null;
#[ORM\Column(length: 120)]
#[Groups(['bovine-type:read', 'reception:read'])]
#[Groups(['bovine-type:read', 'reception:read', 'reception-bovine:read'])]
private ?string $label = null;
#[ORM\Column(length: 50)]
#[Groups(['bovine-type:read', 'reception:read'])]
#[Groups(['bovine-type:read', 'reception:read', 'reception-bovine:read'])]
private ?string $code = null;
public function getId(): ?int

View File

@@ -72,27 +72,27 @@ class Reception
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['reception:read'])]
#[Groups(['reception:read', 'reception-bovine:read'])]
private ?int $id = null;
#[ORM\Column(length: 20, nullable: true)]
#[Groups(['reception:read', 'reception:write'])]
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
private ?string $licensePlate = null;
#[ORM\Column(length: 20, unique: true, nullable: true)]
#[Groups(['reception:read'])]
#[Groups(['reception:read', 'reception-bovine:read'])]
private ?string $identificationNumber = null;
#[ORM\Column(options: ['default' => 0])]
#[Groups(['reception:read', 'reception:write'])]
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
private int $currentStep = 0;
#[ORM\Column(options: ['default' => false])]
#[Groups(['reception:read', 'reception:write'])]
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
private bool $isValid = false;
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
#[Groups(['reception:read', 'reception:write'])]
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $receptionDate = null;

View File

@@ -4,15 +4,21 @@ declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity]
#[ApiFilter(SearchFilter::class, properties: ['reception' => 'exact'])]
#[ORM\UniqueConstraint(name: 'uniq_reception_bovine_type', columns: ['reception_id', 'bovine_type_id'])]
#[ApiResource(
operations: [
new Get(
@@ -26,6 +32,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
normalizationContext: ['groups' => ['reception-bovine:read']],
denormalizationContext: ['groups' => ['reception-bovine:write']],
),
new Patch(
normalizationContext: ['groups' => ['reception-bovine:read']],
denormalizationContext: ['groups' => ['reception-bovine:write']],
),
new Delete(),
],
security: "is_granted('ROLE_USER')",
@@ -36,20 +46,27 @@ class ReceptionBovine
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['reception-bovine:read', 'reception:read'])]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'bovines_types')]
#[Groups(['reception-bovine:read'])]
#[Groups(['reception-bovine:read', 'reception-bovine:write'])]
private ?Reception $reception = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['reception-bovine:read', 'reception:read'])]
private ?BovineType $BovineType = null;
#[Groups(['reception-bovine:read', 'reception-bovine:write', 'reception:read'])]
#[ApiProperty(readableLink: true)]
private ?BovineType $bovineType = null;
#[ORM\Column(options: ['default' => 0])]
#[Groups(['reception-bovine:read', 'reception:read'])]
private ?int $Quantity = null;
// #[Assert\Range(
// min: 0,
// max: 10,
// notInRangeMessage: 'La quantité doit être comprise entre {{ min }} et {{ max }}.'
// )]
#[Groups(['reception-bovine:read', 'reception-bovine:write', 'reception:read'])]
private ?int $quantity = null;
public function getId(): ?int
{
@@ -70,24 +87,24 @@ class ReceptionBovine
public function getBovineType(): ?BovineType
{
return $this->BovineType;
return $this->bovineType;
}
public function setBovineType(?BovineType $BovineType): static
public function setBovineType(?BovineType $bovineType): static
{
$this->BovineType = $BovineType;
$this->bovineType = $bovineType;
return $this;
}
public function getQuantity(): ?int
{
return $this->Quantity;
return $this->quantity;
}
public function setQuantity(int $Quantity): static
public function setQuantity(int $quantity): static
{
$this->Quantity = $Quantity;
$this->quantity = $quantity;
return $this;
}