Compare commits

..

2 Commits

Author SHA1 Message Date
941e3d2258 Merge branch 'refs/heads/develop' into feat/poc-identification-bovin
# Conflicts:
#	.idea/workspace.xml
#	frontend/i18n/locales/fr.json
#	frontend/layouts/default.vue
2026-02-13 10:47:29 +01:00
6421419812 feat : Ajout d'un écran pour afficher les informations d'un bovin 2026-02-02 11:35:10 +01:00
26 changed files with 216 additions and 554 deletions

View File

@@ -46,7 +46,7 @@ Ajouter dans le fichier .env du frontend
* [#324] Creation page admin listing clients * [#324] Creation page admin listing clients
* [#326] Admin modification creation client * [#326] Admin modification creation client
* [#325] Correction diverses * [#325] Correction diverses
* [#319] Réflexion sur des loaders de type skeleton
### Changed ### Changed
### Fixed ### Fixed

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.0.46' app.version: '0.0.44'

View File

@@ -1,6 +1,5 @@
<template> <template>
<skeletonForm v-if="isPageLoading"/> <form @submit.prevent="validate">
<form v-else @submit.prevent="validate">
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16"> <div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1> <h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1>
<!-- Nom de l'utilisateur --> <!-- Nom de l'utilisateur -->
@@ -148,7 +147,6 @@ import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine"; import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
import type {ReceptionFormData} from "~/services/dto/reception-data"; import type {ReceptionFormData} from "~/services/dto/reception-data";
const isPageLoading = ref(true)
const router = useRouter() const router = useRouter()
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const form = reactive<ReceptionFormData>({ const form = reactive<ReceptionFormData>({
@@ -331,17 +329,15 @@ const setDefaultUser = () => {
// On récupère toutes les données des selects au chargement du composant // On récupère toutes les données des selects au chargement du composant
onMounted(async () => { onMounted(async () => {
receptionTypes.value = await getReceptionTypeList() receptionTypes.value = await getReceptionTypeList()
await loadUsers() await loadUsers()
await loadSuppliers() await loadSuppliers()
await loadTrucks() await loadTrucks()
await loadCarriers() await loadCarriers()
await loadDrivers() await loadDrivers()
await loadVehicles() await loadVehicles()
await authStore.ensureSession() await authStore.ensureSession()
setDefaultUser() setDefaultUser()
//isPageLoading.value = false
}) })
// Ajuste driver/vehicle quand le transporteur change (logique LIOT) // Ajuste driver/vehicle quand le transporteur change (logique LIOT)

View File

@@ -1,6 +1,5 @@
<template> <template>
<skeletonForm v-if="isPageLoading"/> <form @submit.prevent="validate">
<form v-else @submit.prevent="validate">
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16"> <div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Expédition</h1> <h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Expédition</h1>
<!-- Nom de l'utilisateur --> <!-- Nom de l'utilisateur -->
@@ -50,7 +49,7 @@
label="Client" label="Client"
:options="customers.map((customer) => ({ :options="customers.map((customer) => ({
value: String(customer.id), value: String(customer.id),
label: customer.name || `Client #${customer.id}` label: customer.label
}))" }))"
:loading="isLoadingCustomers" :loading="isLoadingCustomers"
wrapper-class="col-start-1 row-start-5" wrapper-class="col-start-1 row-start-5"
@@ -124,7 +123,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
<button <button
type="submit" type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
>Valider >Valider
</button> </button>
</div> </div>
@@ -149,12 +148,13 @@ import type {ShipmentFormData} from '~/services/dto/shipment-data'
import {SUPPLIER_CODE} from "~/utils/constants" import {SUPPLIER_CODE} from "~/utils/constants"
import {useAuthStore} from '~/stores/auth' import {useAuthStore} from '~/stores/auth'
import {useShipmentStore} from '~/stores/shipment' import {useShipmentStore} from '~/stores/shipment'
import {computed, reactive, ref, watch, onMounted} from 'vue' import { computed, reactive, ref, watch, onMounted } from 'vue'
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data"; import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
import {getShipmentTypeList} from "~/services/shipment-type"; import {getShipmentTypeList} from "~/services/shipment-type";
import { import {
createShipmentBovine, createShipmentBovine,
deleteShipmentBovine, deleteShipmentBovine,
getBovinShipmentList,
updateShipmentBovine updateShipmentBovine
} from "~/services/bovin-shipment"; } from "~/services/bovin-shipment";
@@ -164,7 +164,7 @@ const trucks = ref<TruckData[]>([])
const carriers = ref<CarrierData[]>([]) const carriers = ref<CarrierData[]>([])
const drivers = ref<DriverData[]>([]) const drivers = ref<DriverData[]>([])
const vehicles = ref<VehicleData[]>([]) const vehicles = ref<VehicleData[]>([])
const isPageLoading = ref(true)
const isLoadingUsers = ref(false) const isLoadingUsers = ref(false)
const isLoadingShipmentTypes = ref(false) const isLoadingShipmentTypes = ref(false)
const isLoadingCustomers = ref(false) const isLoadingCustomers = ref(false)
@@ -310,7 +310,6 @@ onMounted(async () => {
await loadDrivers() await loadDrivers()
await authStore.ensureSession() await authStore.ensureSession()
setDefaultUser() setDefaultUser()
isPageLoading.value = false
}) })
// Hydrate le formulaire depuis l'expédition en cours // Hydrate le formulaire depuis l'expédition en cours
watch( watch(

View File

@@ -1,17 +0,0 @@
<template>
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
<UiSkeletonBlock height="48px"/>
<div class="flex flex-col gap-2" v-for="i in 9">
<UiSkeletonBlock width="96px"/>
<UiSkeletonBlock width="100%" height="42px"/>
</div>
</div>
<div class="flex justify-center">
<UiSkeletonBlock
width="272px"
height="50px"
/>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div class="ps-20">
<UiSkeletonBlock height="48px" class="my-4"/>
<div class="grid grid-cols-3 justify-evenly bg-slate-100 py-4">
<UiSkeletonBlock v-for="i in 3"/>
</div>
<div
class="grid grid-cols-3 gap-4 px-4 py-4 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
v-for="i in 3">
<UiSkeletonBlock v-for="i in 3"/>
</div>
</div>
</template>

View File

@@ -1,22 +0,0 @@
<template>
<div
:class="['animate-pulse', rounded, customClass]"
:style="{ width, height, background }"
/>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
width?: string
height?: string
rounded?: string
background?: string
customClass?: string
}>(), {
width: '50%',
height: '1rem',
rounded: 'rounded-md',
background: '#e5e7eb',
customClass: ''
})
</script>

View File

@@ -46,6 +46,6 @@ definePageMeta({
}) })
onMounted(async () => { onMounted(async () => {
carrierList.value = await getCarrierList() carrierList.value = await getCarrierList(false)
}) })
</script> </script>

View File

@@ -15,9 +15,8 @@
</div> </div>
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12"> <div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
<UiTextInput id="customer-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin"/> <UiTextInput id="customer-label" v-model="form.label" label="Nom du client" :disabled="!auth.isAdmin"/>
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin"/> <UiTextInput id="customer-code" v-model="form.code" label="Code" :disabled="!auth.isAdmin"/>
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin"/>
</div> </div>
<div class="mx-24 mb-4 py-6 border-t border-black"></div> <div class="mx-24 mb-4 py-6 border-t border-black"></div>
@@ -95,9 +94,8 @@ const resolveId = (param: unknown) => {
const customerId = computed(() => resolveId(route.params.id)) const customerId = computed(() => resolveId(route.params.id))
const isLoading = ref(false) const isLoading = ref(false)
const form = reactive<CustomerFormData>({ const form = reactive<CustomerFormData>({
name: "", label: "",
phone: "", code: "",
email: "",
addresses: [], addresses: [],
}) })
@@ -124,9 +122,8 @@ const goToEditAddress = (addressId: number | null) => {
const hydrateFromCustomer = (customer: CustomerData | null) => { const hydrateFromCustomer = (customer: CustomerData | null) => {
if (!customer) return if (!customer) return
form.name = customer.name ?? "" form.label = customer.label ?? ""
form.phone = customer.phone ?? "" form.code = customer.code ?? ""
form.email = customer.email ?? ""
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) { if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
form.addresses = [] form.addresses = []
return return
@@ -168,14 +165,12 @@ async function validate() {
isLoading.value = true isLoading.value = true
try { try {
const name = form.name.trim() const label = form.label.trim()
const phone = form.phone?.trim() || null const code = form.code.trim()
const email = form.email?.trim() || null
const customerPayload: CustomerPayload = { const customerPayload: CustomerPayload = {
name, label,
phone, code,
email,
} }
let targetId: number | null = null let targetId: number | null = null

View File

@@ -14,11 +14,10 @@
<div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16"> <div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16">
<div class="max-h-96 overflow-y-auto"> <div class="max-h-96 overflow-y-auto">
<div <div
class="sticky top-0 z-10 grid grid-cols-8 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide" class="sticky top-0 z-10 grid grid-cols-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
> >
<div>Nom</div> <div>Nom</div>
<div>Téléphone</div> <div>Code</div>
<div>Email</div>
<div>Rue</div> <div>Rue</div>
<div>Complément</div> <div>Complément</div>
<div>Code Postal</div> <div>Code Postal</div>
@@ -27,18 +26,17 @@
</div> </div>
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400"> <div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
Aucun client. Aucun fournisseur.
</div> </div>
<div v-for="customer in customerList" :key="customer.id"> <div v-for="customer in customerList" :key="customer.id">
<div <div
v-if="!customer.addresses || customer.addresses.length === 0" v-if="!customer.addresses || customer.addresses.length === 0"
class="grid grid-cols-8 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer" class="grid grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
@click="goToCustomer(customer.id)" @click="goToCustomer(customer.id)"
> >
<div class="truncate">{{ customer.name || "—" }}</div> <div class="truncate">{{ customer.label }}</div>
<div class="truncate">{{ customer.phone || "—" }}</div> <div class="truncate">{{ customer.code }}</div>
<div class="truncate">{{ customer.email || "—" }}</div>
<div class="col-span-1">Pas d'adresse</div> <div class="col-span-1">Pas d'adresse</div>
<div class="uppercase truncate">{{"—"}}</div> <div class="uppercase truncate">{{"—"}}</div>
<div class="uppercase truncate">{{"—"}}</div> <div class="uppercase truncate">{{"—"}}</div>
@@ -50,15 +48,14 @@
<div <div
v-for="(address, idx) in customer.addresses" v-for="(address, idx) in customer.addresses"
:key="address.id ?? `${customer.id}-${idx}-${address.street}-${address.postalCode}`" :key="address.id ?? `${customer.id}-${idx}-${address.street}-${address.postalCode}`"
class="grid grid-cols-8 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer" class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''" :class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
@click="goToCustomer(customer.id)" @click="goToCustomer(customer.id)"
> >
<div class="truncate"> <div class="truncate">
{{ idx === 0 ? (customer.name || "") : "" }} {{ idx === 0 ? customer.label : "" }}
</div> </div>
<div class="truncate">{{ idx === 0 ? (customer.phone || "") : "" }}</div> <div class="truncate">{{ idx === 0 ? customer.code : "" }}</div>
<div class="truncate">{{ idx === 0 ? (customer.email || "") : "" }}</div>
<div class="truncate">{{ address.street || "" }}</div> <div class="truncate">{{ address.street || "" }}</div>
<div class="truncate">{{ address.street2 || "" }}</div> <div class="truncate">{{ address.street2 || "" }}</div>
<div>{{ address.postalCode || "" }}</div> <div>{{ address.postalCode || "" }}</div>
@@ -69,12 +66,11 @@
<template v-else> <template v-else>
<div <div
class="grid grid-cols-8 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer" class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
@click="goToCustomer(customer.id)" @click="goToCustomer(customer.id)"
> >
<div class="truncate">{{ customer.name || "" }}</div> <div class="truncate">{{ customer.label }}</div>
<div class="truncate">{{ customer.phone || "" }}</div> <div class="truncate">{{ customer.code }}</div>
<div class="truncate">{{ customer.email || "" }}</div>
<div class="col-span-5 text-slate-400"> <div class="col-span-5 text-slate-400">
Adresses non chargées Adresses non chargées
</div> </div>

View File

@@ -0,0 +1,49 @@
<template>
<div>
<div class="flex justify-between">
<h1 class="font-bold text-4xl uppercase">Passeport du bovin</h1>
<p v-if="bovinData">{{ bovinData.presencePeriod }}</p>
</div>
<div class="overflow-x-auto mt-12" v-if="bovinData">
<table class="w-full border-collapse border border-black text-sm">
<tr>
<th
rowspan="2"
class="w-10 border border-black p-0 align-middle"
>
<div class="flex h-full w-full items-center justify-center">
<span class="-rotate-90 whitespace-nowrap font-semibold tracking-widest">
VEAU
</span>
</div>
</th>
<th class="border border-black px-4 py-3 text-center font-semibold">N° de travail</th>
<th class="border border-black px-4 py-3 text-center font-semibold">Sexe</th>
<th class="border border-black px-4 py-3 text-center font-semibold">Code Race</th>
<th class="border border-black px-4 py-3 text-center font-semibold">Code pays</th>
<th class="border border-black px-4 py-3 text-center font-semibold">Type de racial</th>
<th class="border border-black px-4 py-3 text-center font-semibold">Date de naissance</th>
</tr>
<tr>
<th class="border border-black px-4 py-3 text-center font-semibold">{{ bovinData.workNumber }}</th>
<th class="border border-black px-4 py-3 text-center font-semibold">{{ bovinData.sex }}</th>
<th class="border border-black px-4 py-3 text-center font-semibold">{{ bovinData.breedType }}</th>
<th class="border border-black px-4 py-3 text-center font-semibold">FR {{ bovinData.numeroNational }}</th>
<th class="border border-black px-4 py-3 text-center font-semibold">???</th>
<th class="border border-black px-4 py-3 text-center font-semibold">{{ bovinData.birthDate }}</th>
</tr>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import {getBovinData} from "~/services/identification-bovin";
const bovinData = ref<IdentificationBovinData|null>()
onMounted(async () => {
bovinData.value = await getBovinData('7979580026');
})
</script>

View File

@@ -1,10 +1,10 @@
<template> <template>
<skeletonTable v-if="isPageLoading"/> <div class="flex items-center justify-start gap-10">
<div v-else class="ps-20"> <Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
<div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
<h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1> <h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1>
</div> </div>
<div class="ps-20 " >
<div class="mt-6 border border-slate-200 mb-16 "> <div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"> <div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Numéro</div> <div>Numéro</div>
@@ -39,7 +39,7 @@ import {getReceptionList} from "~/services/reception";
const receptionList = ref<ReceptionData[]>() const receptionList = ref<ReceptionData[]>()
const router = useRouter() const router = useRouter()
const isPageLoading = ref(true)
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => { const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => {
const entry = reception.weights?.find((weight) => weight.type === type) const entry = reception.weights?.find((weight) => weight.type === type)
if (!entry || entry.weight == null || entry.dsd == null) { if (!entry || entry.weight == null || entry.dsd == null) {
@@ -54,6 +54,5 @@ const goToReception = (id: number) => {
onMounted(async () => { onMounted(async () => {
receptionList.value = await getReceptionList(true) receptionList.value = await getReceptionList(true)
isPageLoading.value = false
}) })
</script> </script>

View File

@@ -1,11 +1,10 @@
<template> <template>
<skeletonTable v-if="isPageLoading"/>
<div v-else class="ps-20 ">
<div class="flex items-center justify-start gap-10"> <div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/> <Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
<h1 class="text-3xl font-bold uppercase">listes des expéditions finie</h1> <h1 class="text-3xl font-bold uppercase">listes des expéditions finie</h1>
</div> </div>
<div class="ps-20 ">
<div class="mt-6 border border-slate-200 mb-16 "> <div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"> <div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Numéro</div> <div>Numéro</div>
@@ -51,7 +50,7 @@ import {getShipmentList} from "~/services/shipment";
const shipmentList = ref<ShipmentData[]>() const shipmentList = ref<ShipmentData[]>()
const router = useRouter() const router = useRouter()
const isPageLoading = ref(true)
const formatWeighing = (shipment: ShipmentData, type: 'gross' | 'tare') => { const formatWeighing = (shipment: ShipmentData, type: 'gross' | 'tare') => {
const entry = shipment.weights?.find((weight) => weight.type === type) const entry = shipment.weights?.find((weight) => weight.type === type)
if (!entry || entry.weight == null || entry.dsd == null) { if (!entry || entry.weight == null || entry.dsd == null) {
@@ -74,11 +73,9 @@ const formatBovinShipmentLines = (shipment: ShipmentData) => {
const goToshipment = (id: number) => { const goToshipment = (id: number) => {
//router.push(`/shipment/update/${id}`) //router.push(`/shipment/update/${id}`)
} }
onMounted(async () => { onMounted(async () => {
shipmentList.value = await getShipmentList(true) shipmentList.value = await getShipmentList(true)
isPageLoading.value = false
}) })
</script> </script>

View File

@@ -4,22 +4,19 @@ export type CustomerAddresses = AddressFormData[] | string[]
export interface CustomerData { export interface CustomerData {
id: number id: number
name: string label: string
phone?: string | null code?: string | null
email?: string | null
addresses: CustomerAddresses addresses: CustomerAddresses
} }
export interface CustomerFormData { export interface CustomerFormData {
name: string label: string
phone?: string code?: string
email?: string
addresses: AddressFormData[] addresses: AddressFormData[]
} }
export type CustomerPayload = { export type CustomerPayload = {
name: string label: string
phone?: string | null code?: string | null
email?: string | null
addresses?: string[] addresses?: string[]
} }

View File

@@ -0,0 +1,22 @@
interface IdentificationBovinData {
numeroNational: string,
sex: string | null,
breedType: string | null,
workNumber: string | null,
birthDate: string | null,
birthDateCompletenessFlag: string | null,
isFilie: boolean | null,
motherNationalNumber: string | null,
motherBreedType: string | null,
fatherNationalNumber: string | null,
fatherBreedType: string | null,
birthExploitationNumber: string | null,
presencePeriod: PresencePeriod[]
}
interface PresencePeriod {
entryDate: string | null,
entryCause: string | null,
exitDate: string | null,
exitCause: string | null
}

View File

@@ -0,0 +1,25 @@
import { useApi } from '~/composables/useApi'
export type BovinDataResponse =
| IdentificationBovinData
| { 'hydra:member'?: IdentificationBovinData }
export async function getBovinData(
nationalNumber: string
): Promise<IdentificationBovinData | null> {
const api = useApi()
const response = await api.get<BovinDataResponse>(
`bovins/${nationalNumber}/identification`,
{},
{ toastErrorKey: 'errors.building.list' }
)
if (response && typeof response === 'object') {
// direct item
if (!('hydra:member' in response)) return response as IdentificationBovinData
// hydra format
if (response['hydra:member']) return response['hydra:member']
}
return null
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260213093000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add name, phone and email fields to customer.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE customer ADD name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer ADD phone VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer ADD email VARCHAR(255) DEFAULT NULL');
$this->addSql('UPDATE customer SET name = label WHERE name IS NULL');
$this->addSql('ALTER TABLE customer ALTER COLUMN name SET NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE customer DROP name');
$this->addSql('ALTER TABLE customer DROP phone');
$this->addSql('ALTER TABLE customer DROP email');
}
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260213101500 extends AbstractMigration
{
public function getDescription(): string
{
return 'Align customer with supplier: keep name/email/phone and drop label/code.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE customer ALTER COLUMN name TYPE VARCHAR(180)');
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(180)');
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(40)');
$this->addSql('ALTER TABLE customer DROP COLUMN label');
$this->addSql('ALTER TABLE customer DROP COLUMN code');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE customer ADD label VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE customer ADD code VARCHAR(255) DEFAULT NULL');
$this->addSql('UPDATE customer SET label = name WHERE label IS NULL');
$this->addSql("UPDATE customer SET code = regexp_replace(upper(name), '[^A-Z0-9]+', '_', 'g') WHERE code IS NULL");
$this->addSql('ALTER TABLE customer ALTER COLUMN label SET NOT NULL');
$this->addSql('ALTER TABLE customer ALTER COLUMN code SET NOT NULL');
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(255)');
}
}

View File

@@ -15,7 +15,8 @@ use App\State\BovinIdentificationProvider;
uriTemplate: '/bovins/{numeroNational}/identification', uriTemplate: '/bovins/{numeroNational}/identification',
provider: BovinIdentificationProvider::class provider: BovinIdentificationProvider::class
), ),
] ],
security: "is_granted('ROLE_USER')",
)] )]
final class BovinIdentification final class BovinIdentification
{ {

View File

@@ -5,15 +5,12 @@ declare(strict_types=1);
namespace App\Command; namespace App\Command;
use App\Entity\Address; use App\Entity\Address;
use App\Entity\BovineType;
use App\Entity\Building; use App\Entity\Building;
use App\Entity\Carrier; use App\Entity\Carrier;
use App\Entity\Customer;
use App\Entity\Driver; use App\Entity\Driver;
use App\Entity\MerchandiseType; use App\Entity\MerchandiseType;
use App\Entity\PelletType; use App\Entity\PelletType;
use App\Entity\ReceptionType; use App\Entity\ReceptionType;
use App\Entity\ShipmentType;
use App\Entity\Supplier; use App\Entity\Supplier;
use App\Entity\Truck; use App\Entity\Truck;
use App\Entity\Vehicle; use App\Entity\Vehicle;
@@ -53,11 +50,7 @@ class SeedCommand extends Command
$this->seedPelletTypes(); $this->seedPelletTypes();
$this->seedBuildings(); $this->seedBuildings();
$this->seedReceptionTypes(); $this->seedReceptionTypes();
$this->seedBovineTypes();
$this->seedShipmentTypes();
$this->seedSuppliers(); $this->seedSuppliers();
$this->entityManager->flush();
$this->seedCustomers($io);
$this->entityManager->flush(); $this->entityManager->flush();
@@ -68,7 +61,7 @@ class SeedCommand extends Command
private function seedTrucks(): array private function seedTrucks(): array
{ {
$trucks = ['Citerne', 'Porteur', 'Plateau', 'Remorque', 'Benne']; $trucks = ['Citerne', 'Porteur'];
$citerne = null; $citerne = null;
$porteur = null; $porteur = null;
foreach ($trucks as $name) { foreach ($trucks as $name) {
@@ -230,39 +223,6 @@ class SeedCommand extends Command
} }
} }
private function seedBovineTypes(): void
{
$bovineTypes = [
['label' => 'Limousine', 'code' => '34'],
['label' => 'Charolaise', 'code' => '38'],
['label' => 'Parthenaise', 'code' => '71'],
];
foreach ($bovineTypes as $type) {
$this->upsertByCode(BovineType::class, $type['code'], static function (BovineType $entity) use ($type) {
$entity
->setLabel($type['label'])
->setCode($type['code'])
;
});
}
}
private function seedShipmentTypes(): void
{
$shipmentTypes = [
['label' => 'Bovin de boucherie', 'code' => 'BDB'],
['label' => "Bovin d'équarrissage", 'code' => 'BE'],
];
foreach ($shipmentTypes as $type) {
$this->upsertByCode(ShipmentType::class, $type['code'], static function (ShipmentType $entity) use ($type) {
$entity
->setLabel($type['label'])
->setCode($type['code'])
;
});
}
}
private function seedSuppliers(): void private function seedSuppliers(): void
{ {
$suppliers = [ $suppliers = [
@@ -498,130 +458,6 @@ class SeedCommand extends Command
} }
} }
private function seedCustomers(SymfonyStyle $io): void
{
$addressRepo = $this->entityManager->getRepository(Address::class);
$customers = [
[
'name' => 'ARNAULT EURL',
'phone' => '05.49.02.65.27',
'email' => 'eurl.arnault86@orange.fr',
'addresses' => [
[
'label' => 'ARNAULT EURL',
'street' => 'Moulin du Guéret',
'street2' => 'B.P 30425',
'postalCode' => '86100',
'city' => 'Antran',
'countryCode' => 'FR',
],
],
],
[
'name' => 'COVILIM',
'phone' => '05.55.30.03.10',
'email' => 'sandra.robineaux@covilim.com',
'addresses' => [
[
'label' => 'COVILIM',
'street' => 'Rue de Nexon',
'street2' => null,
'postalCode' => '87000',
'city' => 'LIMOGES',
'countryCode' => 'FR',
],
],
],
[
'name' => 'Les producteurs de la marche (LPM)',
'phone' => '05.55.63.04.53',
'email' => 'f.legalliard@lpmcoop.fr',
'addresses' => [
[
'label' => 'Les producteurs de la marche (LPM)',
'street' => 'Rue de Nexon',
'street2' => null,
'postalCode' => '87000',
'city' => 'LIMOGES',
'countryCode' => 'FR',
],
],
],
[
'name' => 'LORTHOLARY BETAIL',
'phone' => '05.49.52.77.10',
'email' => 'contact86@lortholarybetail.com',
'addresses' => [
[
'label' => 'LORTHOLARY BETAIL',
'street' => 'FERME DE GENIEC',
'street2' => null,
'postalCode' => '86550',
'city' => 'MIGNALOUX BEAUVOIR',
'countryCode' => 'FR',
],
],
],
[
'name' => 'TERRENA',
'phone' => '02.51.67.17.98',
'email' => null,
'addresses' => [
[
'label' => 'TERRENA',
'street' => 'La Blanchardière',
'street2' => null,
'postalCode' => '44522',
'city' => 'MESANGER',
'countryCode' => 'FR',
],
],
],
];
foreach ($customers as $customerData) {
$customerName = $customerData['name'] ?? $customerData['label'] ?? null;
if (!$customerName) {
$io->warning('Customer skipped: missing "name".');
continue;
}
$customer = $this->upsertByName(Customer::class, $customerName, static function (Customer $customer) use ($customerData, $customerName) {
$customer
->setName($customerName)
->setPhone($customerData['phone'] ?? null)
->setEmail($customerData['email'] ?? null)
;
});
$addresses = [];
if (isset($customerData['addresses']) && is_array($customerData['addresses'])) {
foreach ($customerData['addresses'] as $addressData) {
$addresses[] = $this->upsertAddress($addressData);
}
} else {
// Backward compatibility for older seed format with address ids.
$addressIds = $customerData['addressIds'] ?? (isset($customerData['addressId']) ? [$customerData['addressId']] : []);
foreach ($addressIds as $addressId) {
$address = $addressRepo->find($addressId);
if (!$address instanceof Address) {
$io->warning(sprintf(
'Customer "%s" skipped address id %d: not found.',
$customerName,
$addressId
));
continue;
}
$addresses[] = $address;
}
}
$customer->setAddresses($addresses);
$this->entityManager->persist($customer);
}
}
private function upsertByCode(string $entityClass, string $code, callable $apply): object private function upsertByCode(string $entityClass, string $code, callable $apply): object
{ {
$repo = $this->entityManager->getRepository($entityClass); $repo = $this->entityManager->getRepository($entityClass);

View File

@@ -20,6 +20,7 @@ class AppFixtures extends Fixture implements DependentFixtureInterface
return [ return [
TransportFixtures::class, TransportFixtures::class,
ReferenceFixtures::class, ReferenceFixtures::class,
SupplierFixtures::class,
UserFixtures::class, UserFixtures::class,
]; ];
} }

View File

@@ -5,13 +5,10 @@ declare(strict_types=1);
namespace App\DataFixtures; namespace App\DataFixtures;
use App\Entity\Address; use App\Entity\Address;
use App\Entity\BovineType;
use App\Entity\Building; use App\Entity\Building;
use App\Entity\Customer;
use App\Entity\MerchandiseType; use App\Entity\MerchandiseType;
use App\Entity\PelletType; use App\Entity\PelletType;
use App\Entity\ReceptionType; use App\Entity\ReceptionType;
use App\Entity\ShipmentType;
use App\Entity\Supplier; use App\Entity\Supplier;
use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\ObjectManager;
@@ -20,8 +17,6 @@ class ReferenceFixtures extends Fixture
{ {
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
$addressIndex = [];
$merchandiseTypes = [ $merchandiseTypes = [
['label' => 'Foin', 'code' => 'FOIN'], ['label' => 'Foin', 'code' => 'FOIN'],
['label' => 'Paille', 'code' => 'PAILLE'], ['label' => 'Paille', 'code' => 'PAILLE'],
@@ -74,31 +69,6 @@ class ReferenceFixtures extends Fixture
$manager->persist($receptionType); $manager->persist($receptionType);
} }
$bovineTypes = [
['label' => 'Limousine', 'code' => '34'],
['label' => 'Charolaise', 'code' => '38'],
['label' => 'Parthenaise', 'code' => '71'],
];
foreach ($bovineTypes as $type) {
$bovineType = new BovineType()
->setLabel($type['label'])
->setCode($type['code'])
;
$manager->persist($bovineType);
}
$shipmentTypes = [
['label' => 'Bovin de boucherie', 'code' => 'BDB'],
['label' => "Bovin d'équarrissage", 'code' => 'BE'],
];
foreach ($shipmentTypes as $type) {
$shipmentType = new ShipmentType()
->setLabel($type['label'])
->setCode($type['code'])
;
$manager->persist($shipmentType);
}
$suppliers = [ $suppliers = [
[ [
'name' => 'LIOT', 'name' => 'LIOT',
@@ -320,129 +290,21 @@ class ReferenceFixtures extends Fixture
; ;
foreach ($supplierData['addresses'] as $addressData) { foreach ($supplierData['addresses'] as $addressData) {
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']); $address = new Address()
if (!isset($addressIndex[$addressKey])) { ->setLabel($addressData['label'])
$addressIndex[$addressKey] = new Address() ->setStreet($addressData['street'])
->setLabel($addressData['label']) ->setStreet2($addressData['street2'])
->setStreet($addressData['street']) ->setPostalCode($addressData['postalCode'])
->setStreet2($addressData['street2']) ->setCity($addressData['city'])
->setPostalCode($addressData['postalCode']) ->setCountryCode($addressData['countryCode'])
->setCity($addressData['city']) ;
->setCountryCode($addressData['countryCode']) $manager->persist($address);
;
$manager->persist($addressIndex[$addressKey]);
}
$address = $addressIndex[$addressKey];
$supplier->getAddresses()->add($address); $supplier->getAddresses()->add($address);
} }
$manager->persist($supplier); $manager->persist($supplier);
} }
$customers = [
[
'name' => 'ARNAULT EURL',
'phone' => '05.49.02.65.27',
'email' => 'eurl.arnault86@orange.fr',
'addresses' => [
[
'label' => 'ARNAULT EURL',
'street' => 'Moulin du Guéret',
'street2' => 'B.P 30425',
'postalCode' => '86100',
'city' => 'Antran',
'countryCode' => 'FR',
],
],
],
[
'name' => 'COVILIM',
'phone' => '05.55.30.03.10',
'email' => 'sandra.robineaux@covilim.com',
'addresses' => [
[
'label' => 'COVILIM',
'street' => 'Rue de Nexon',
'street2' => null,
'postalCode' => '87000',
'city' => 'LIMOGES',
'countryCode' => 'FR',
],
],
],
[
'name' => 'Les producteurs de la marche (LPM)',
'phone' => '05.55.63.04.53',
'email' => 'f.legalliard@lpmcoop.fr',
'addresses' => [
[
'label' => 'Les producteurs de la marche (LPM)',
'street' => 'Rue de Nexon',
'street2' => null,
'postalCode' => '87000',
'city' => 'LIMOGES',
'countryCode' => 'FR',
],
],
],
[
'name' => 'LORTHOLARY BETAIL',
'phone' => '05.49.52.77.10',
'email' => 'contact86@lortholarybetail.com',
'addresses' => [
[
'label' => 'LORTHOLARY BETAIL',
'street' => 'FERME DE GENIEC',
'street2' => null,
'postalCode' => '86550',
'city' => 'MIGNALOUX BEAUVOIR',
'countryCode' => 'FR',
],
],
],
[
'name' => 'TERRENA',
'phone' => '02.51.67.17.98',
'email' => null,
'addresses' => [
[
'label' => 'TERRENA',
'street' => 'La Blanchardière',
'street2' => null,
'postalCode' => '44522',
'city' => 'MESANGER',
'countryCode' => 'FR',
],
],
],
];
foreach ($customers as $customerData) {
$customer = new Customer()
->setName($customerData['name'])
->setPhone($customerData['phone'])
->setEmail($customerData['email'])
;
foreach ($customerData['addresses'] as $addressData) {
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']);
if (!isset($addressIndex[$addressKey])) {
$addressIndex[$addressKey] = new Address()
->setLabel($addressData['label'])
->setStreet($addressData['street'])
->setStreet2($addressData['street2'])
->setPostalCode($addressData['postalCode'])
->setCity($addressData['city'])
->setCountryCode($addressData['countryCode'])
;
$manager->persist($addressIndex[$addressKey]);
}
$customer->getAddresses()->add($addressIndex[$addressKey]);
}
$manager->persist($customer);
}
$manager->flush(); $manager->flush();
} }
} }

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Address;
use App\Entity\Supplier;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class SupplierFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
$address = new Address()
->setLabel('LIOT CHATELLERAULT')
->setStreet("14 Allée d'Argenson")
->setStreet2('ZI Nord')
->setPostalCode('86100')
->setCity('CHATELLERAULT')
->setCountryCode('FR')
;
$supplier = new Supplier()
->setName('LIOT')
->setEmail('lpc.contacts@lpc-liot.fr')
->setPhone('05.49.20.09.10')
;
$supplier->getAddresses()->add($address);
$manager->persist($address);
$manager->persist($supplier);
$manager->flush();
}
}

View File

@@ -15,17 +15,11 @@ class TransportFixtures extends Fixture
{ {
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
$citerne = new Truck()->setName('Citerne'); $citerne = new Truck()->setName('Citerne');
$porteur = new Truck()->setName('Porteur'); $porteur = new Truck()->setName('Porteur');
$plateau = new Truck()->setName('Plateau');
$remorque = new Truck()->setName('Remorque');
$benne = new Truck()->setName('Benne');
$manager->persist($citerne); $manager->persist($citerne);
$manager->persist($porteur); $manager->persist($porteur);
$manager->persist($plateau);
$manager->persist($remorque);
$manager->persist($benne);
$liot = new Carrier() $liot = new Carrier()
->setName('LIOT') ->setName('LIOT')

View File

@@ -47,17 +47,13 @@ class Customer
#[Groups(['shipment:read', 'customer:read'])] #[Groups(['shipment:read', 'customer:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180)] #[ORM\Column(length: 255)]
#[Groups(['customer:read', 'customer:write', 'shipment:read'])] #[Groups(['customer:read', 'customer:write', 'shipment:read'])]
private string $name = ''; private ?string $label = null;
#[ORM\Column(length: 180, nullable: true)] #[ORM\Column(length: 255)]
#[Groups(['customer:read', 'customer:write', 'shipment:read'])] #[Groups(['customer:read', 'customer:write', 'shipment:read'])]
private ?string $email = null; private ?string $code = null;
#[ORM\Column(length: 40, nullable: true)]
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
private ?string $phone = null;
/** /**
* @var Collection<int, Address> * @var Collection<int, Address>
@@ -78,40 +74,24 @@ class Customer
return $this->id; return $this->id;
} }
public function getName(): string public function getLabel(): ?string
{ {
return $this->name; return $this->label;
} }
public function setName(string $name): self public function setLabel(?string $label): void
{ {
$this->name = $name; $this->label = $label;
return $this;
} }
public function getEmail(): ?string public function getCode(): ?string
{ {
return $this->email; return $this->code;
} }
public function setEmail(?string $email): self public function setCode(?string $code): void
{ {
$this->email = $email; $this->code = $code;
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(?string $phone): self
{
$this->phone = $phone;
return $this;
} }
public function getAddresses(): Collection public function getAddresses(): Collection

View File

@@ -26,10 +26,6 @@ use Symfony\Component\Serializer\Attribute\Groups;
), ),
new GetCollection( new GetCollection(
normalizationContext: ['groups' => ['supplier:read']], normalizationContext: ['groups' => ['supplier:read']],
),
new GetCollection(
uriTemplate: '/admin/suppliers',
normalizationContext: ['groups' => ['supplier:read']],
security: "is_granted('ROLE_ADMIN')" security: "is_granted('ROLE_ADMIN')"
), ),
new Post( new Post(