Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db8973336 | ||
|
|
5be38b9f5a | ||
|
|
07bcd6270d | ||
|
|
422a46482a | ||
|
|
f498f4dc95 | ||
|
|
7690cd6484 | ||
|
|
a736e233df | ||
| b24b5fb145 | |||
|
|
8d25d3e589 | ||
| 40f8bb40c9 | |||
| c84aa27d2c | |||
| 77b9323615 | |||
| 6bf194b280 | |||
| cdc9c33f4e | |||
| b45e2d3a95 |
@@ -63,6 +63,7 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#FER-12] Ajouter un blocage des utilisateurs
|
* [#FER-12] Ajouter un blocage des utilisateurs
|
||||||
* [#FER-13] Faire des recherches sur le scanner des bêtes
|
* [#FER-13] Faire des recherches sur le scanner des bêtes
|
||||||
* [#FER-15] Les non-admin ne peuvent plus supprimer de réception/expédition en attente
|
* [#FER-15] Les non-admin ne peuvent plus supprimer de réception/expédition en attente
|
||||||
|
* [#FER-17] Ecran d'ajout de bovin
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -1,68 +1,87 @@
|
|||||||
# Projet Ferme
|
# Projet Ferme t
|
||||||
|
|
||||||
## Installation du projet
|
## Installation du projet
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
|
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
|
||||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
|
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
Pour linux, il faut installer docker et nvm.
|
Pour linux, il faut installer docker et nvm.
|
||||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
|
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
|
||||||
|
|
||||||
### Installation du projet
|
### Installation du projet
|
||||||
|
|
||||||
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make start
|
make start
|
||||||
make install
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
||||||
|
|
||||||
### Configuration global
|
### Configuration global
|
||||||
|
|
||||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
||||||
|
|
||||||
Vérifier que dans le .env.local, vous avez :
|
Vérifier que dans le .env.local, vous avez :
|
||||||
* APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
|
||||||
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
- APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||||
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
- DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
* PONT_BASCULE_URL
|
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
* JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
- PONT_BASCULE_URL
|
||||||
* JWT_PUBLIC_KEY
|
- JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||||
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
- JWT_PUBLIC_KEY
|
||||||
* COOKIE_SECURE=0 (en dev 0 et en prod 1. Si c'est du http, laisser en 0)
|
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||||
|
- COOKIE_SECURE=0 (en dev 0 et en prod 1. Si c'est du http, laisser en 0)
|
||||||
|
|
||||||
Vérifier que dans le .env du dossier frontend, vous avez :
|
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||||
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
|
||||||
|
- NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
||||||
|
|
||||||
### Configuration xdebug
|
### Configuration xdebug
|
||||||
|
|
||||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||||
* Name : ferme-docker
|
|
||||||
* Host : localhost
|
- Name : ferme-docker
|
||||||
* Port : 8080
|
- Host : localhost
|
||||||
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
- Port : 8080
|
||||||
|
- Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||||
|
|
||||||
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
||||||
|
|
||||||
## Utilisation du projet
|
## Utilisation du projet
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
L'api est disponible sur http://localhost:8080/api
|
L'api est disponible sur http://localhost:8080/api
|
||||||
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
||||||
Vous pouvez modifier le port si nécessaire.
|
Vous pouvez modifier le port si nécessaire.
|
||||||
|
|
||||||
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
||||||
C'est un bdd local dans le docker.
|
C'est un bdd local dans le docker.
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev-nuxt
|
make dev-nuxt
|
||||||
```
|
```
|
||||||
|
|
||||||
Le front sera accessible sur http://localhost:3000
|
Le front sera accessible sur http://localhost:3000
|
||||||
|
|
||||||
### Authentification
|
### Authentification
|
||||||
|
|
||||||
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
||||||
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
||||||
|
|
||||||
### Login flow
|
### Login flow
|
||||||
|
|
||||||
- Frontend envoie les identifiants à:
|
- Frontend envoie les identifiants à:
|
||||||
- `POST /api/login_check`
|
- `POST /api/login_check`
|
||||||
- Backend returns:
|
- Backend returns:
|
||||||
@@ -72,63 +91,90 @@ Le frontend ne lit jamais directement le token, le navigateur envoie automatique
|
|||||||
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
||||||
|
|
||||||
### Fixtures
|
### Fixtures
|
||||||
|
|
||||||
Pour lancer les fixtures (Attention sa purge la bdd complètement)
|
Pour lancer les fixtures (Attention sa purge la bdd complètement)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php bin/console doctrine:fixtures:load
|
php bin/console doctrine:fixtures:load
|
||||||
```
|
```
|
||||||
|
|
||||||
Attention cette commande est dangereuse, à utiliser que pour les débuts de la prod ou en recette.
|
Attention cette commande est dangereuse, à utiliser que pour les débuts de la prod ou en recette.
|
||||||
Dans un premier temps pour remplir les listes, vous pouvez lancer la commande symfony
|
Dans un premier temps pour remplir les listes, vous pouvez lancer la commande symfony
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php bin/console app:seed
|
php bin/console app:seed
|
||||||
```
|
```
|
||||||
|
|
||||||
La commande va faire une update ou une création en fonction des data existante.
|
La commande va faire une update ou une création en fonction des data existante.
|
||||||
|
|
||||||
## Livraison en recette
|
## Livraison en recette
|
||||||
|
|
||||||
### Préparatifs
|
### Préparatifs
|
||||||
|
|
||||||
Avant de déployer, il faut penser à ajouter les variables d'env s'il y a des changements/modifications.
|
Avant de déployer, il faut penser à ajouter les variables d'env s'il y a des changements/modifications.
|
||||||
Le .env se trouve /var/www/ferme/.env
|
Le .env se trouve /var/www/ferme/.env
|
||||||
|
|
||||||
Le script de livraison est version dans le repo dans script/deploy-release.sh <br>
|
Le script de livraison est version dans le repo dans script/deploy-release.sh <br>
|
||||||
Sur la machine, il est disponible dans /usr/local/bin/deploy-ferme <br>
|
Sur la machine, il est disponible dans /usr/local/bin/deploy-ferme <br>
|
||||||
Pour le modifier, il faut copier le contenu du deploy-release.sh dans le deploy-ferme
|
Pour le modifier, il faut copier le contenu du deploy-release.sh dans le deploy-ferme
|
||||||
|
|
||||||
### Livraison
|
### Livraison
|
||||||
|
|
||||||
Sur le serveur de recette, il suffit d'utiliser cette commande pour livrer
|
Sur le serveur de recette, il suffit d'utiliser cette commande pour livrer
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/usr/local/bin/deploy-ferme vX.Y.Z
|
/usr/local/bin/deploy-ferme vX.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commandes utiles
|
## Commandes utiles
|
||||||
|
|
||||||
Pour restart le container
|
Pour restart le container
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make restart
|
make restart
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour lancer les TU
|
Pour lancer les TU
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour accéder au container et lance des commandes
|
Pour accéder au container et lance des commandes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make shell
|
make shell
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour clear le cache Symfony
|
Pour clear le cache Symfony
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make cache-clear
|
make cache-clear
|
||||||
```
|
```
|
||||||
|
|
||||||
Faire une migration
|
Faire une migration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make migration-migrate
|
make migration-migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour générer un password pour un user
|
Pour générer un password pour un user
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make shell
|
make shell
|
||||||
php bin/console security:hash-password
|
php bin/console security:hash-password
|
||||||
```
|
```
|
||||||
|
|
||||||
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO "user" (username, roles, password)
|
INSERT INTO "user" (username, roles, password)
|
||||||
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gestion des logs
|
## Gestion des logs
|
||||||
|
|
||||||
Pour suivre les logs en temps réel :
|
Pour suivre les logs en temps réel :
|
||||||
* tail -f var/log/dev.log
|
|
||||||
* tail -f var/log/prod.log
|
- tail -f var/log/dev.log
|
||||||
|
- tail -f var/log/prod.log
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.81'
|
app.version: '0.0.82'
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
|||||||
entityName: options.entityName,
|
entityName: options.entityName,
|
||||||
apiResource: options.apiResource,
|
apiResource: options.apiResource,
|
||||||
titleLabel: options.titleLabel,
|
titleLabel: options.titleLabel,
|
||||||
|
isFinal: options.isFinal,
|
||||||
getWeightFromScale: options.getWeightFromScale,
|
getWeightFromScale: options.getWeightFromScale,
|
||||||
updateEntity: options.updateEntity,
|
updateEntity: options.updateEntity,
|
||||||
loadEntity: options.loadEntity
|
loadEntity: options.loadEntity
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface UseWeighingOptions {
|
|||||||
entityName: 'reception' | 'shipment'
|
entityName: 'reception' | 'shipment'
|
||||||
apiResource: string
|
apiResource: string
|
||||||
titleLabel: string
|
titleLabel: string
|
||||||
|
isFinal?: boolean
|
||||||
getWeightFromScale: () => Promise<WeightData>
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
updateEntity: (id: number, payload: any) => Promise<any>
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
loadEntity?: (id: number) => Promise<any>
|
loadEntity?: (id: number) => Promise<any>
|
||||||
@@ -23,6 +24,7 @@ export const useWeighing = ({
|
|||||||
entityName,
|
entityName,
|
||||||
apiResource,
|
apiResource,
|
||||||
titleLabel,
|
titleLabel,
|
||||||
|
isFinal = false,
|
||||||
getWeightFromScale,
|
getWeightFromScale,
|
||||||
updateEntity,
|
updateEntity,
|
||||||
loadEntity
|
loadEntity
|
||||||
@@ -77,7 +79,7 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = mode === 'tare'
|
const nextStep = isFinal
|
||||||
? entity.value.currentStep
|
? entity.value.currentStep
|
||||||
: entity.value.currentStep + 1
|
: entity.value.currentStep + 1
|
||||||
await updateEntity(entity.value.id, {
|
await updateEntity(entity.value.id, {
|
||||||
@@ -152,7 +154,7 @@ export const useWeighingShipment = ({
|
|||||||
entity: shipment,
|
entity: shipment,
|
||||||
entityName: 'shipment',
|
entityName: 'shipment',
|
||||||
apiResource: 'shipments',
|
apiResource: 'shipments',
|
||||||
titleLabel: modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein',
|
titleLabel: modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide',
|
||||||
getWeightFromScale: async () => {
|
getWeightFromScale: async () => {
|
||||||
const { getWeightShipment } = await import('~/services/shipment')
|
const { getWeightShipment } = await import('~/services/shipment')
|
||||||
return getWeightShipment()
|
return getWeightShipment()
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export const shipmentConfig: WorkflowConfig = {
|
|||||||
apiResource: 'shipments',
|
apiResource: 'shipments',
|
||||||
steps: [
|
steps: [
|
||||||
{ label: 'Expédition' },
|
{ label: 'Expédition' },
|
||||||
{ label: 'Pesée à vide', weighingMode: 'gross' },
|
{ label: 'Pesée à vide', weighingMode: 'tare' },
|
||||||
{ label: 'Chargement' },
|
{ label: 'Chargement' },
|
||||||
{ label: 'Pesée à plein', weighingMode: 'tare', isFinal: true }
|
{ label: 'Pesée à plein', weighingMode: 'gross', isFinal: true }
|
||||||
],
|
],
|
||||||
weighingLabels: {
|
weighingLabels: {
|
||||||
gross: 'Pesée à vide',
|
gross: 'Pesée à plein',
|
||||||
tare: 'Pesée à plein'
|
tare: 'Pesée à vide'
|
||||||
},
|
},
|
||||||
buildReceiptFilename: (entity: WorkflowEntity) => {
|
buildReceiptFilename: (entity: WorkflowEntity) => {
|
||||||
const ship = entity as any
|
const ship = entity as any
|
||||||
|
|||||||
180
frontend/pages/infrastructure/bovine.vue
Normal file
180
frontend/pages/infrastructure/bovine.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="goBack"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ isEdit ? 'Modification d\'un bovin' : 'Ajout d\'un bovin' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput
|
||||||
|
id="bovine-national-number"
|
||||||
|
v-model="form.nationalNumber"
|
||||||
|
label="Numéro national"
|
||||||
|
:disabled="!auth.isAdmin || isLoading"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiNumberInput
|
||||||
|
id="bovine-received-weight"
|
||||||
|
v-model="form.receivedWeight"
|
||||||
|
label="Poids à l'arrivée (kg)"
|
||||||
|
:min="0"
|
||||||
|
:disabled="!auth.isAdmin || isLoading"
|
||||||
|
wrapper-class="w-[280px] flex-col"
|
||||||
|
label-class="font-bold uppercase"
|
||||||
|
/>
|
||||||
|
<UiDateInput
|
||||||
|
id="bovine-arrival-date"
|
||||||
|
v-model="form.arrivalDate"
|
||||||
|
label="Date d'arrivée"
|
||||||
|
:disabled="!auth.isAdmin || isLoading"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11">
|
||||||
|
<UiSelect
|
||||||
|
id="bovine-supplier"
|
||||||
|
v-model="form.supplierId"
|
||||||
|
label="Vendeur"
|
||||||
|
:options="supplierOptions"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
:disabled="!auth.isAdmin || isLoading"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
<div class="w-[280px]" />
|
||||||
|
<div class="w-[280px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
:disabled="!auth.isAdmin || isLoading"
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
<Icon :name="isEdit ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ isEdit ? 'Valider' : 'Ajouter' }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createBovine, getBovine, updateBovine } from '~/services/bovine'
|
||||||
|
import type { BovinePayload } from '~/services/dto/bovine-data'
|
||||||
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||||
|
import { getSupplierList } from '~/services/supplier'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const caseId = computed(() => {
|
||||||
|
const raw = Number(route.query.caseId)
|
||||||
|
return Number.isFinite(raw) && raw > 0 ? raw : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const bovineId = computed(() => {
|
||||||
|
const raw = Number(route.query.id)
|
||||||
|
return Number.isFinite(raw) && raw > 0 ? raw : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEdit = computed(() => bovineId.value !== null)
|
||||||
|
|
||||||
|
const form = reactive<{
|
||||||
|
nationalNumber: string
|
||||||
|
receivedWeight: number | null
|
||||||
|
arrivalDate: string | null
|
||||||
|
supplierId: string
|
||||||
|
}>({
|
||||||
|
nationalNumber: '',
|
||||||
|
receivedWeight: null,
|
||||||
|
arrivalDate: null,
|
||||||
|
supplierId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const suppliers = ref<SupplierData[]>([])
|
||||||
|
const isLoadingSuppliers = ref(false)
|
||||||
|
|
||||||
|
const supplierOptions = computed(() =>
|
||||||
|
suppliers.value.map(s => ({ value: String(s.id), label: s.name }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const backRoute = computed(() => ({
|
||||||
|
path: '/infrastructure/case',
|
||||||
|
query: caseId.value ? { id: String(caseId.value) } : {}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push(backRoute.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSuppliers = async () => {
|
||||||
|
isLoadingSuppliers.value = true
|
||||||
|
try {
|
||||||
|
suppliers.value = await getSupplierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingSuppliers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrate = async () => {
|
||||||
|
if (!isEdit.value || bovineId.value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const bovine = await getBovine(bovineId.value)
|
||||||
|
form.nationalNumber = bovine.nationalNumber ?? ''
|
||||||
|
form.receivedWeight = bovine.receivedWeight ?? null
|
||||||
|
form.arrivalDate = bovine.arrivalDate ?? null
|
||||||
|
if (bovine.supplier) {
|
||||||
|
const supplierId = bovine.supplier.replace(/.*\//, '')
|
||||||
|
form.supplierId = supplierId
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
if (isLoading.value || !auth.isAdmin) return
|
||||||
|
if (!caseId.value) return
|
||||||
|
if (!form.nationalNumber.trim()) return
|
||||||
|
|
||||||
|
const payload: BovinePayload = {
|
||||||
|
nationalNumber: form.nationalNumber.trim(),
|
||||||
|
receivedWeight: form.receivedWeight,
|
||||||
|
arrivalDate: form.arrivalDate,
|
||||||
|
buildingCase: `/api/building_cases/${caseId.value}`,
|
||||||
|
supplier: form.supplierId ? `/api/suppliers/${form.supplierId}` : null
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value && bovineId.value !== null) {
|
||||||
|
await updateBovine(bovineId.value, payload)
|
||||||
|
} else {
|
||||||
|
await createBovine(payload)
|
||||||
|
}
|
||||||
|
router.push(backRoute.value)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadSuppliers)
|
||||||
|
watch(bovineId, hydrate, { immediate: true })
|
||||||
|
</script>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
v-for="cell in entry.cells"
|
v-for="cell in entry.cells"
|
||||||
:key="cell.key"
|
:key="cell.key"
|
||||||
class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none"
|
class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none"
|
||||||
:class="[cell.sideBorderClass, activeLegendStatutId !== null && cell.caseStatusId !== activeLegendStatutId ? 'opacity-35 hover:opacity-70' : '']"
|
:class="[cell.sideBorderClass, activeLegendLabel !== null && cell.caseStatusLabel !== activeLegendLabel ? 'opacity-35 hover:opacity-70' : '']"
|
||||||
:style="[cell.spanStyle, cell.sideBorderStyle]"
|
:style="[cell.spanStyle, cell.sideBorderStyle]"
|
||||||
:to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'"
|
:to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'"
|
||||||
:title="cell.caseStatusLabel ?? undefined"
|
:title="cell.caseStatusLabel ?? undefined"
|
||||||
@@ -58,25 +58,19 @@
|
|||||||
|
|
||||||
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<!-- 3 zones fixes pour forcer gauche / centre / droite sur toute la largeur -->
|
<div class="flex gap-6">
|
||||||
<div class="grid w-full grid-cols-3 gap-3">
|
|
||||||
<div
|
<div
|
||||||
v-for="(statut, index) in statutLegend"
|
v-for="statut in statutLegend"
|
||||||
:key="statut.id"
|
:key="statut.label"
|
||||||
class="flex min-w-0 cursor-pointer items-center gap-2 py-1"
|
class="flex cursor-pointer items-center gap-2 py-1"
|
||||||
:class="[
|
@mouseenter="activeLegendLabel = statut.label"
|
||||||
index === 0 ? 'justify-self-start' : '',
|
@mouseleave="activeLegendLabel = null"
|
||||||
index === statutLegend.length - 1 ? 'justify-self-end' : '',
|
|
||||||
index > 0 && index < statutLegend.length - 1 ? 'justify-self-center' : ''
|
|
||||||
]"
|
|
||||||
@mouseenter="activeLegendStatutId = statut.id"
|
|
||||||
@mouseleave="activeLegendStatutId = null"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="h-5 w-5 border border-slate-300"
|
class="h-5 w-5 border border-slate-300"
|
||||||
:style="statut.couleur ? { backgroundColor: statut.couleur } : {}"
|
:style="statut.couleur ? { backgroundColor: statut.couleur } : {}"
|
||||||
></span>
|
></span>
|
||||||
<span class="truncate text-sm uppercase text-slate-700">
|
<span class="text-sm uppercase text-slate-700">
|
||||||
{{ statut.label }}
|
{{ statut.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,33 +84,35 @@
|
|||||||
import type {BuildingData} from "~/services/dto/building-data"
|
import type {BuildingData} from "~/services/dto/building-data"
|
||||||
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
|
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
|
||||||
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
|
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
|
||||||
import type {BuildingCaseStatusData} from "~/services/dto/building-case-status-data"
|
|
||||||
import {getBuildingList} from "~/services/building"
|
import {getBuildingList} from "~/services/building"
|
||||||
import {getStatutList} from "~/services/statut"
|
|
||||||
|
|
||||||
definePageMeta({layout: "default"})
|
definePageMeta({layout: "default"})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// Données brutes chargées depuis l'API
|
// Données brutes chargées depuis l'API
|
||||||
const buildingList = ref<BuildingData[]>([])
|
const buildingList = ref<BuildingData[]>([])
|
||||||
const statutLegend = ref<BuildingCaseStatusData[]>([])
|
const statutLegend = [
|
||||||
|
{ label: 'Libre', couleur: '#A3B18A' },
|
||||||
|
{ label: 'Occupé', couleur: '#3A506B' },
|
||||||
|
{ label: 'Malade', couleur: '#E07A5F' },
|
||||||
|
]
|
||||||
// Statut actuellement survolé dans la légende (pour filtrage visuel)
|
// Statut actuellement survolé dans la légende (pour filtrage visuel)
|
||||||
const activeLegendStatutId = ref<number | null>(null)
|
const activeLegendLabel = ref<string | null>(null)
|
||||||
// Modèle de vue prêt pour le template (layout + cellules + styles de grille)
|
// Modèle de vue prêt pour le template (layout + cellules + styles de grille)
|
||||||
const buildingLayouts = computed(() =>
|
const buildingLayouts = computed(() =>
|
||||||
buildingList.value.map((building) => {
|
buildingList.value
|
||||||
// On affiche uniquement le premier layout du bâtiment
|
.filter((building) => building.layouts && building.layouts.length > 0)
|
||||||
const layout = building.layouts?.[0] ?? null
|
.map((building) => {
|
||||||
const view = layout ? buildLayoutView(layout) : null
|
const layout = building.layouts![0]
|
||||||
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
|
const view = buildLayoutView(layout)
|
||||||
})
|
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
type GridCell = {
|
type GridCell = {
|
||||||
key: string
|
key: string
|
||||||
caseId: number | null
|
caseId: number | null
|
||||||
display: string
|
display: string
|
||||||
caseStatusId: number | null
|
|
||||||
caseStatusLabel: string | null
|
caseStatusLabel: string | null
|
||||||
// Couleur de fond de la case (dépend du statut)
|
// Couleur de fond de la case (dépend du statut)
|
||||||
caseStyle?: Record<string, string>
|
caseStyle?: Record<string, string>
|
||||||
@@ -130,7 +126,8 @@ type GridCell = {
|
|||||||
contentInsetClass: string
|
contentInsetClass: string
|
||||||
}
|
}
|
||||||
// Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite
|
// Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite
|
||||||
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number }
|
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number}
|
||||||
|
|
||||||
|
|
||||||
// Nettoie la couleur de statut pour éviter les chaînes vides / espaces
|
// Nettoie la couleur de statut pour éviter les chaînes vides / espaces
|
||||||
const normalizeCaseStatusColor = (value: string | null | undefined): string | null => {
|
const normalizeCaseStatusColor = (value: string | null | undefined): string | null => {
|
||||||
@@ -181,7 +178,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
|
|||||||
// Métadonnées utiles au rendu / navigation / légende
|
// Métadonnées utiles au rendu / navigation / légende
|
||||||
const caseId = (position.buildingCase?.id ?? null) as number | null
|
const caseId = (position.buildingCase?.id ?? null) as number | null
|
||||||
const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null
|
const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null
|
||||||
const caseStatusId = position.buildingCase?.statut?.id ?? null
|
|
||||||
const caseStatusLabel = position.buildingCase?.statut?.label ?? null
|
const caseStatusLabel = position.buildingCase?.statut?.label ?? null
|
||||||
const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur)
|
const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur)
|
||||||
|
|
||||||
@@ -191,7 +187,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
|
|||||||
columnSpan,
|
columnSpan,
|
||||||
caseId,
|
caseId,
|
||||||
display: caseNumber !== null ? String(caseNumber) : "Case",
|
display: caseNumber !== null ? String(caseNumber) : "Case",
|
||||||
caseStatusId,
|
|
||||||
caseStatusLabel,
|
caseStatusLabel,
|
||||||
caseStyle: statusColor ? {backgroundColor: statusColor} : undefined,
|
caseStyle: statusColor ? {backgroundColor: statusColor} : undefined,
|
||||||
// Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne
|
// Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne
|
||||||
@@ -230,13 +225,6 @@ const buildLayoutView = (layout: BuildingLayoutData): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Chargement initial des bâtiments et de la légende des statuts
|
buildingList.value = await getBuildingList()
|
||||||
const buildings = await getBuildingList()
|
|
||||||
const statuts = await getStatutList()
|
|
||||||
buildingList.value = buildings
|
|
||||||
// Tri alphabétique FR pour une légende stable
|
|
||||||
statutLegend.value = [...statuts].sort((a, b) =>
|
|
||||||
(a.label ?? "").localeCompare(b.label ?? "", "fr", {sensitivity: "base"})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center items-center">
|
<div class="px-[86px]">
|
||||||
<UiButton
|
<div class="flex items-center justify-between relative">
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
:disabled="!hasCaseId"
|
<Icon
|
||||||
@click="printCaseReport"
|
@click="router.push('/infrastructure/building')"
|
||||||
>
|
name="gg:arrow-left-o"
|
||||||
Imprimer
|
size="44"
|
||||||
</UiButton>
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<h1 class="font-bold text-4xl text-primary-500 uppercase">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
v-if="hasCaseId"
|
||||||
|
class="bg-primary-500 p-1 rounded-md flex items-center cursor-pointer"
|
||||||
|
title="Imprimer"
|
||||||
|
@click="printCaseReport"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:printer-outline" size="32" class="text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="hasCaseId"
|
||||||
|
:to="addBovineRoute"
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-6 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60 pointer-events-none'"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 border border-slate-200 mb-16">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-3 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Numéro national</div>
|
||||||
|
<div>Poids à l'arrivée (kg)</div>
|
||||||
|
<div>Date d'arrivée</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="bovines.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="bovine in bovines"
|
||||||
|
:key="bovine.id"
|
||||||
|
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm border-t border-slate-200"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer hover:bg-slate-50' : ''"
|
||||||
|
:role="auth.isAdmin ? 'button' : undefined"
|
||||||
|
:tabindex="auth.isAdmin ? 0 : undefined"
|
||||||
|
@click="goToBovine(bovine.id)"
|
||||||
|
@keydown.enter="goToBovine(bovine.id)"
|
||||||
|
>
|
||||||
|
<div>{{ bovine.nationalNumber }}</div>
|
||||||
|
<div>{{ bovine.receivedWeight ?? '—' }}</div>
|
||||||
|
<div>{{ formatDate(bovine.arrivalDate) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="px-4 py-3 text-sm border-t border-slate-200 text-slate-500"
|
||||||
|
>
|
||||||
|
Aucun bovin dans cette case.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { BuildingCaseData } from '~/services/dto/building-case-data'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const { printPdf } = usePdfPrinter()
|
const { printPdf } = usePdfPrinter()
|
||||||
|
const api = useApi()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const caseId = computed(() => Number(route.query.id))
|
const caseId = computed(() => Number(route.query.id))
|
||||||
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
|
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
|
||||||
|
|
||||||
|
const buildingCase = ref<BuildingCaseData | null>(null)
|
||||||
|
const bovines = computed(() => buildingCase.value?.bovines ?? [])
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
if (!buildingCase.value) return ''
|
||||||
|
const buildingLabel = buildingCase.value.building?.label ?? ''
|
||||||
|
const caseNumber = buildingCase.value.caseNumber ?? ''
|
||||||
|
return `${buildingLabel} case ${caseNumber}`.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const addBovineRoute = computed(() => ({
|
||||||
|
path: '/infrastructure/bovine',
|
||||||
|
query: { caseId: String(caseId.value) }
|
||||||
|
}))
|
||||||
|
|
||||||
|
const formatDate = (date: string | null) => {
|
||||||
|
if (!date) return '—'
|
||||||
|
const d = new Date(date)
|
||||||
|
if (isNaN(d.getTime())) return date
|
||||||
|
return d.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCase = async () => {
|
||||||
|
if (!hasCaseId.value) {
|
||||||
|
buildingCase.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buildingCase.value = await api.get<BuildingCaseData>(`/building_cases/${caseId.value}`)
|
||||||
|
}
|
||||||
|
|
||||||
const printCaseReport = async () => {
|
const printCaseReport = async () => {
|
||||||
if (!hasCaseId.value) {
|
if (!hasCaseId.value) {
|
||||||
return
|
return
|
||||||
@@ -24,4 +121,14 @@ const printCaseReport = async () => {
|
|||||||
const filename = `tableau_poids_case_${caseId.value}.pdf`
|
const filename = `tableau_poids_case_${caseId.value}.pdf`
|
||||||
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
|
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToBovine = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: '/infrastructure/bovine',
|
||||||
|
query: { id: String(id), caseId: String(caseId.value) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(caseId, loadCase, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||||
<WorkflowWeight
|
<WorkflowWeight
|
||||||
v-if="storeShipment?.currentStep === 1"
|
v-if="storeShipment?.currentStep === 1"
|
||||||
ref="grossWeightRef"
|
ref="tareWeightRef"
|
||||||
mode="gross"
|
mode="tare"
|
||||||
entity-name="shipment"
|
entity-name="shipment"
|
||||||
api-resource="shipments"
|
api-resource="shipments"
|
||||||
:title-label="shipmentConfig.weighingLabels.gross"
|
:title-label="shipmentConfig.weighingLabels.tare"
|
||||||
:is-final="false"
|
:is-final="false"
|
||||||
:entity="storeShipment"
|
:entity="storeShipment"
|
||||||
:get-weight-from-scale="getWeightShipment"
|
:get-weight-from-scale="getWeightShipment"
|
||||||
@@ -34,11 +34,11 @@
|
|||||||
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||||
<WorkflowWeight
|
<WorkflowWeight
|
||||||
v-if="storeShipment?.currentStep === 3"
|
v-if="storeShipment?.currentStep === 3"
|
||||||
ref="tareWeightRef"
|
ref="grossWeightRef"
|
||||||
mode="tare"
|
mode="gross"
|
||||||
entity-name="shipment"
|
entity-name="shipment"
|
||||||
api-resource="shipments"
|
api-resource="shipments"
|
||||||
:title-label="shipmentConfig.weighingLabels.tare"
|
:title-label="shipmentConfig.weighingLabels.gross"
|
||||||
:is-final="true"
|
:is-final="true"
|
||||||
:entity="storeShipment"
|
:entity="storeShipment"
|
||||||
:get-weight-from-scale="getWeightShipment"
|
:get-weight-from-scale="getWeightShipment"
|
||||||
|
|||||||
@@ -149,16 +149,6 @@
|
|||||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||||
:class="[
|
|
||||||
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
|
||||||
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
|
||||||
]"
|
|
||||||
@click="activeTab = 'weights'"
|
|
||||||
>
|
|
||||||
pesée à plein
|
|
||||||
</h1>
|
|
||||||
<h1
|
|
||||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
|
||||||
:class="[
|
:class="[
|
||||||
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
@@ -167,6 +157,16 @@
|
|||||||
>
|
>
|
||||||
pesée à vide
|
pesée à vide
|
||||||
</h1>
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
||||||
|
:class="[
|
||||||
|
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesée à plein
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<update-weight
|
<update-weight
|
||||||
@@ -248,7 +248,7 @@ const hasTareWeightError = computed(() =>
|
|||||||
submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null)
|
submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null)
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeTab = ref<'weightsEmpty' | 'weights'>('weights')
|
const activeTab = ref<'weightsEmpty' | 'weights'>('weightsEmpty')
|
||||||
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
const formIsLoading = ref(false)
|
const formIsLoading = ref(false)
|
||||||
|
|||||||
@@ -27,3 +27,16 @@ export async function createBovines(nationalNumbers: string[]): Promise<{ create
|
|||||||
|
|
||||||
return { created, errors }
|
return { created, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBovine(id: number) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<BovineData>(`bovines/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBovine(id: number, payload: BovinePayload) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<BovineData>(`bovines/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.bovine.update',
|
||||||
|
toastSuccessKey: 'success.bovine.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface BovineData {
|
|||||||
receivedWeight: number | null
|
receivedWeight: number | null
|
||||||
arrivalDate: string | null
|
arrivalDate: string | null
|
||||||
buildingCase: string | null
|
buildingCase: string | null
|
||||||
|
supplier: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BovinePayload = {
|
export type BovinePayload = {
|
||||||
@@ -11,4 +12,5 @@ export type BovinePayload = {
|
|||||||
receivedWeight?: number | null
|
receivedWeight?: number | null
|
||||||
arrivalDate?: string | null
|
arrivalDate?: string | null
|
||||||
buildingCase?: string | null
|
buildingCase?: string | null
|
||||||
|
supplier?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data'
|
import type { BovineData } from '~/services/dto/bovine-data'
|
||||||
|
|
||||||
|
export interface BuildingSummary {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BuildingCaseData {
|
export interface BuildingCaseData {
|
||||||
id: number
|
id: number
|
||||||
caseNumber: number | null
|
caseNumber: number | null
|
||||||
code: string | null
|
code: string | null
|
||||||
capacity: number | null
|
capacity: number | null
|
||||||
statut?: BuildingCaseStatusData | null
|
statut?: { label: string; couleur: string } | null
|
||||||
|
building?: BuildingSummary | null
|
||||||
|
bovines: BovineData[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface BuildingCaseStatusData {
|
|
||||||
id: number
|
|
||||||
label: string | null
|
|
||||||
code: string | null
|
|
||||||
couleur: string | null
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
|
||||||
import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data'
|
|
||||||
|
|
||||||
export type StatutListResponse =
|
|
||||||
| BuildingCaseStatusData[]
|
|
||||||
| { 'hydra:member'?: BuildingCaseStatusData[] }
|
|
||||||
|
|
||||||
export async function getStatutList(): Promise<BuildingCaseStatusData[]> {
|
|
||||||
const api = useApi()
|
|
||||||
const response = await api.get<StatutListResponse>('statuts', {}, {
|
|
||||||
toastErrorKey: 'errors.http.get'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
|
||||||
return response['hydra:member']
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
35
migrations/Version20260410065020.php
Normal file
35
migrations/Version20260410065020.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 Version20260410065020 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 bovine ADD supplier_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE bovine ADD CONSTRAINT FK_2068337F2ADD6D8C FOREIGN KEY (supplier_id) REFERENCES supplier (id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2068337F2ADD6D8C ON bovine (supplier_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE bovine DROP CONSTRAINT FK_2068337F2ADD6D8C');
|
||||||
|
$this->addSql('DROP INDEX IDX_2068337F2ADD6D8C');
|
||||||
|
$this->addSql('ALTER TABLE bovine DROP supplier_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
migrations/Version20260410074533.php
Normal file
35
migrations/Version20260410074533.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 Version20260410074533 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 bovine ADD work_number VARCHAR(50) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE bovine ADD birth_date DATE DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE bovine ADD breed_code VARCHAR(20) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE bovine DROP work_number');
|
||||||
|
$this->addSql('ALTER TABLE bovine DROP birth_date');
|
||||||
|
$this->addSql('ALTER TABLE bovine DROP breed_code');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
migrations/Version20260410081839.php
Normal file
35
migrations/Version20260410081839.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 Version20260410081839 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 building_case DROP CONSTRAINT fk_de2cee50f6203804');
|
||||||
|
$this->addSql('DROP INDEX idx_de2cee50f6203804');
|
||||||
|
$this->addSql('ALTER TABLE building_case DROP statut_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE building_case ADD statut_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE building_case ADD CONSTRAINT fk_de2cee50f6203804 FOREIGN KEY (statut_id) REFERENCES statut (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX idx_de2cee50f6203804 ON building_case (statut_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
migrations/Version20260410082723.php
Normal file
31
migrations/Version20260410082723.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?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 Version20260410082723 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('DROP TABLE statut');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE statut (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, color VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/Command/EnrichBovinesCommand.php
Normal file
99
src/Command/EnrichBovinesCommand.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\Bovine;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:enrich-bovines',
|
||||||
|
description: 'Enrichit les bovins existants avec les données EdNotif (n° travail, date naissance, race).'
|
||||||
|
)]
|
||||||
|
class EnrichBovinesCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly BovinApiInterface $bovinApi,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$bovines = $this->entityManager->getRepository(Bovine::class)->findBy(['workNumber' => null]);
|
||||||
|
|
||||||
|
if (0 === count($bovines)) {
|
||||||
|
$io->success('Tous les bovins sont déjà enrichis.');
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->info(sprintf('%d bovin(s) à enrichir.', count($bovines)));
|
||||||
|
|
||||||
|
$enriched = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($bovines as $bovine) {
|
||||||
|
try {
|
||||||
|
$animalFile = $this->bovinApi->getAnimalFile(
|
||||||
|
nationalNumber: $bovine->getNationalNumber(),
|
||||||
|
countryCode: 'FR',
|
||||||
|
);
|
||||||
|
$identification = $animalFile->identification;
|
||||||
|
|
||||||
|
if (null === $identification) {
|
||||||
|
$io->warning(sprintf(' %s — pas d\'identification retournée.', $bovine->getNationalNumber()));
|
||||||
|
++$failed;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bovine->setWorkNumber($identification->workNumber);
|
||||||
|
$bovine->setBirthDate($identification->birthDate?->date);
|
||||||
|
$bovine->setBreedCode($this->normalizeBreedCode($identification->breedType));
|
||||||
|
|
||||||
|
++$enriched;
|
||||||
|
$io->text(sprintf(' ✓ %s → n° travail %s', $bovine->getNationalNumber(), $identification->workNumber ?? '—'));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
++$failed;
|
||||||
|
$io->warning(sprintf(' %s — erreur : %s', $bovine->getNationalNumber(), $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$io->success(sprintf('%d enrichi(s), %d échoué(s).', $enriched, $failed));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeBreedCode(mixed $breedType): ?string
|
||||||
|
{
|
||||||
|
if (null === $breedType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($breedType)) {
|
||||||
|
return (string) $breedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
|
||||||
|
return $matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ 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\ShipmentType;
|
||||||
use App\Entity\Statut;
|
|
||||||
use App\Entity\Supplier;
|
use App\Entity\Supplier;
|
||||||
use App\Entity\Truck;
|
use App\Entity\Truck;
|
||||||
use App\Entity\Vehicle;
|
use App\Entity\Vehicle;
|
||||||
@@ -230,24 +229,6 @@ class SeedCommand extends Command
|
|||||||
|
|
||||||
private function seedBuildingInfrastructure(): void
|
private function seedBuildingInfrastructure(): void
|
||||||
{
|
{
|
||||||
$statusByCode = [];
|
|
||||||
$statusRows = [
|
|
||||||
['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'],
|
|
||||||
['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'],
|
|
||||||
['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'],
|
|
||||||
];
|
|
||||||
foreach ($statusRows as $statusRow) {
|
|
||||||
/** @var Statut $status */
|
|
||||||
$status = $this->upsertByCode(Statut::class, $statusRow['code'], static function (Statut $entity) use ($statusRow) {
|
|
||||||
$entity
|
|
||||||
->setLabel($statusRow['label'])
|
|
||||||
->setCode($statusRow['code'])
|
|
||||||
->setColor($statusRow['color'])
|
|
||||||
;
|
|
||||||
});
|
|
||||||
$statusByCode[$statusRow['code']] = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
$buildingRepo = $this->entityManager->getRepository(Building::class);
|
$buildingRepo = $this->entityManager->getRepository(Building::class);
|
||||||
$layoutByBuildingCode = [];
|
$layoutByBuildingCode = [];
|
||||||
$layoutRows = [
|
$layoutRows = [
|
||||||
@@ -274,25 +255,15 @@ class SeedCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
$caseRows = [
|
$caseRows = [
|
||||||
['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'status' => 'LB'],
|
['buildingCode' => 'B1', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'status' => 'OC'],
|
['buildingCode' => 'B2', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'status' => 'ML'],
|
['buildingCode' => 'B3', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'status' => 'LB'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'status' => 'OC'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'status' => 'LB'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'status' => 'ML'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'status' => 'OC'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'status' => 'ML'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'status' => 'LB'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'status' => 'OC'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'status' => 'ML'],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$caseByCode = [];
|
$caseByCode = [];
|
||||||
foreach ($caseRows as $caseRow) {
|
foreach ($caseRows as $caseRow) {
|
||||||
$building = $buildingRepo->findOneBy(['code' => $caseRow['buildingCode']]);
|
$building = $buildingRepo->findOneBy(['code' => $caseRow['buildingCode']]);
|
||||||
$status = $statusByCode[$caseRow['status']] ?? null;
|
if (!$building instanceof Building) {
|
||||||
if (!$building instanceof Building || !$status instanceof Statut) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +271,12 @@ class SeedCommand extends Command
|
|||||||
$code = sprintf('%s-C%d', $caseRow['buildingCode'], $caseNumber);
|
$code = sprintf('%s-C%d', $caseRow['buildingCode'], $caseNumber);
|
||||||
|
|
||||||
/** @var BuildingCase $buildingCase */
|
/** @var BuildingCase $buildingCase */
|
||||||
$buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building, $status) {
|
$buildingCase = $this->upsertByCode(BuildingCase::class, $code, static function (BuildingCase $entity) use ($code, $caseNumber, $building) {
|
||||||
$entity
|
$entity
|
||||||
->setCode($code)
|
->setCode($code)
|
||||||
->setCaseNumber($caseNumber)
|
->setCaseNumber($caseNumber)
|
||||||
->setCapacity(15)
|
->setCapacity(15)
|
||||||
->setIdBuilding($building)
|
->setIdBuilding($building)
|
||||||
->setStatut($status)
|
|
||||||
;
|
;
|
||||||
});
|
});
|
||||||
$caseByCode[$code] = $buildingCase;
|
$caseByCode[$code] = $buildingCase;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\Entity\Building;
|
|||||||
use App\Entity\BuildingCase;
|
use App\Entity\BuildingCase;
|
||||||
use App\Entity\BuildingCasePosition;
|
use App\Entity\BuildingCasePosition;
|
||||||
use App\Entity\BuildingLayout;
|
use App\Entity\BuildingLayout;
|
||||||
use App\Entity\Statut;
|
|
||||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
@@ -18,10 +17,9 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
|
|||||||
{
|
{
|
||||||
public function load(ObjectManager $manager): void
|
public function load(ObjectManager $manager): void
|
||||||
{
|
{
|
||||||
$statuts = $this->loadStatuts($manager);
|
|
||||||
$buildings = $this->getBuildingsByCode($manager, ['B1', 'B2', 'B3']);
|
$buildings = $this->getBuildingsByCode($manager, ['B1', 'B2', 'B3']);
|
||||||
$layouts = $this->loadLayouts($manager, $buildings);
|
$layouts = $this->loadLayouts($manager, $buildings);
|
||||||
$cases = $this->loadBuildingCases($manager, $buildings, $statuts);
|
$cases = $this->loadBuildingCases($manager, $buildings);
|
||||||
$this->loadCasePositions($manager, $layouts, $cases);
|
$this->loadCasePositions($manager, $layouts, $cases);
|
||||||
|
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
@@ -34,38 +32,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, Statut>
|
|
||||||
*/
|
|
||||||
private function loadStatuts(ObjectManager $manager): array
|
|
||||||
{
|
|
||||||
$repo = $manager->getRepository(Statut::class);
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
['label' => 'Libre', 'code' => 'LB', 'color' => '#A3B18A'],
|
|
||||||
['label' => 'Occupé', 'code' => 'OC', 'color' => '#3A506B'],
|
|
||||||
['label' => 'Malade', 'code' => 'ML', 'color' => '#E07A5F'],
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($data as $row) {
|
|
||||||
/** @var null|Statut $statut */
|
|
||||||
$statut = $repo->findOneBy(['code' => $row['code']]);
|
|
||||||
if (!$statut instanceof Statut) {
|
|
||||||
$statut = new Statut()
|
|
||||||
->setLabel($row['label'])
|
|
||||||
->setCode($row['code'])
|
|
||||||
->setColor($row['color'])
|
|
||||||
;
|
|
||||||
$manager->persist($statut);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result[$row['code']] = $statut;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<string> $codes
|
* @param list<string> $codes
|
||||||
*
|
*
|
||||||
@@ -126,34 +92,21 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, Building> $buildings
|
* @param array<string, Building> $buildings
|
||||||
* @param array<string, Statut> $statuts
|
|
||||||
*
|
*
|
||||||
* @return array<string, BuildingCase>
|
* @return array<string, BuildingCase>
|
||||||
*/
|
*/
|
||||||
private function loadBuildingCases(ObjectManager $manager, array $buildings, array $statuts): array
|
private function loadBuildingCases(ObjectManager $manager, array $buildings): array
|
||||||
{
|
{
|
||||||
$repo = $manager->getRepository(BuildingCase::class);
|
$repo = $manager->getRepository(BuildingCase::class);
|
||||||
|
|
||||||
$statusRanges = [
|
$caseRanges = [
|
||||||
// B1
|
['buildingCode' => 'B1', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 1, 'to' => 12, 'statut' => 'LB'],
|
['buildingCode' => 'B2', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 13, 'to' => 24, 'statut' => 'OC'],
|
['buildingCode' => 'B3', 'from' => 1, 'to' => 44],
|
||||||
['buildingCode' => 'B1', 'from' => 25, 'to' => 32, 'statut' => 'ML'],
|
|
||||||
['buildingCode' => 'B1', 'from' => 33, 'to' => 44, 'statut' => 'LB'],
|
|
||||||
// B2
|
|
||||||
['buildingCode' => 'B2', 'from' => 1, 'to' => 10, 'statut' => 'OC'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 11, 'to' => 22, 'statut' => 'LB'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 23, 'to' => 30, 'statut' => 'ML'],
|
|
||||||
['buildingCode' => 'B2', 'from' => 31, 'to' => 44, 'statut' => 'OC'],
|
|
||||||
// B3
|
|
||||||
['buildingCode' => 'B3', 'from' => 1, 'to' => 8, 'statut' => 'ML'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 9, 'to' => 20, 'statut' => 'LB'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 21, 'to' => 34, 'statut' => 'OC'],
|
|
||||||
['buildingCode' => 'B3', 'from' => 35, 'to' => 44, 'statut' => 'ML'],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = [];
|
$result = [];
|
||||||
foreach ($statusRanges as $range) {
|
foreach ($caseRanges as $range) {
|
||||||
for ($caseNumber = $range['from']; $caseNumber <= $range['to']; ++$caseNumber) {
|
for ($caseNumber = $range['from']; $caseNumber <= $range['to']; ++$caseNumber) {
|
||||||
$code = sprintf('%s-C%d', $range['buildingCode'], $caseNumber);
|
$code = sprintf('%s-C%d', $range['buildingCode'], $caseNumber);
|
||||||
|
|
||||||
@@ -169,7 +122,6 @@ class BuildingInfrastructureFixtures extends Fixture implements DependentFixture
|
|||||||
->setCode($code)
|
->setCode($code)
|
||||||
->setCapacity(15)
|
->setCapacity(15)
|
||||||
->setIdBuilding($buildings[$range['buildingCode']])
|
->setIdBuilding($buildings[$range['buildingCode']])
|
||||||
->setStatut($statuts[$range['statut']])
|
|
||||||
;
|
;
|
||||||
$manager->persist($buildingCase);
|
$manager->persist($buildingCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Get;
|
|||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
use ApiPlatform\Metadata\Patch;
|
use ApiPlatform\Metadata\Patch;
|
||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\State\BovineProcessor;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Context;
|
use Symfony\Component\Serializer\Attribute\Context;
|
||||||
@@ -31,12 +32,14 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
normalizationContext: ['groups' => ['bovine:read']],
|
normalizationContext: ['groups' => ['bovine:read']],
|
||||||
denormalizationContext: ['groups' => ['bovine:write']],
|
denormalizationContext: ['groups' => ['bovine:write']],
|
||||||
security: "is_granted('ROLE_ADMIN')",
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
processor: BovineProcessor::class,
|
||||||
),
|
),
|
||||||
new Patch(
|
new Patch(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
normalizationContext: ['groups' => ['bovine:read']],
|
normalizationContext: ['groups' => ['bovine:read']],
|
||||||
denormalizationContext: ['groups' => ['bovine:write']],
|
denormalizationContext: ['groups' => ['bovine:write']],
|
||||||
security: "is_granted('ROLE_ADMIN')",
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
processor: BovineProcessor::class,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
@@ -46,19 +49,19 @@ class Bovine
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['bovine:read'])]
|
#[Groups(['bovine:read', 'building_case:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 50)]
|
#[ORM\Column(length: 50)]
|
||||||
#[Groups(['bovine:read', 'bovine:write'])]
|
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||||
private string $nationalNumber = '';
|
private string $nationalNumber = '';
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
#[Groups(['bovine:read', 'bovine:write'])]
|
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||||
private ?int $receivedWeight = null;
|
private ?int $receivedWeight = null;
|
||||||
|
|
||||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||||
#[Groups(['bovine:read', 'bovine:write'])]
|
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
private ?DateTimeImmutable $arrivalDate = null;
|
private ?DateTimeImmutable $arrivalDate = null;
|
||||||
|
|
||||||
@@ -66,6 +69,23 @@ class Bovine
|
|||||||
#[Groups(['bovine:read', 'bovine:write'])]
|
#[Groups(['bovine:read', 'bovine:write'])]
|
||||||
private ?BuildingCase $buildingCase = null;
|
private ?BuildingCase $buildingCase = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||||
|
private ?Supplier $supplier = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 50, nullable: true)]
|
||||||
|
#[Groups(['bovine:read', 'building_case:read'])]
|
||||||
|
private ?string $workNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||||
|
#[Groups(['bovine:read', 'building_case:read'])]
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
|
private ?DateTimeImmutable $birthDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 20, nullable: true)]
|
||||||
|
#[Groups(['bovine:read', 'building_case:read'])]
|
||||||
|
private ?string $breedCode = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -118,4 +138,52 @@ class Bovine
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSupplier(): ?Supplier
|
||||||
|
{
|
||||||
|
return $this->supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplier(?Supplier $supplier): static
|
||||||
|
{
|
||||||
|
$this->supplier = $supplier;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWorkNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->workNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWorkNumber(?string $workNumber): static
|
||||||
|
{
|
||||||
|
$this->workNumber = $workNumber;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBirthDate(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBirthDate(?DateTimeImmutable $birthDate): static
|
||||||
|
{
|
||||||
|
$this->birthDate = $birthDate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBreedCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->breedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBreedCode(?string $breedCode): static
|
||||||
|
{
|
||||||
|
$this->breedCode = $breedCode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ class Building
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['building:read', 'reception:read'])]
|
#[Groups(['building:read', 'building:summary', 'reception:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 120)]
|
#[ORM\Column(length: 120)]
|
||||||
#[Groups(['building:read', 'reception:read'])]
|
#[Groups(['building:read', 'building:summary', 'reception:read'])]
|
||||||
private string $label = '';
|
private string $label = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 50)]
|
#[ORM\Column(length: 50)]
|
||||||
#[Groups(['building:read', 'reception:read'])]
|
#[Groups(['building:read', 'building:summary', 'reception:read'])]
|
||||||
private string $code = '';
|
private string $code = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
|||||||
operations: [
|
operations: [
|
||||||
new Get(
|
new Get(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
normalizationContext: ['groups' => ['building:read']],
|
normalizationContext: ['groups' => ['building_case:read', 'building:summary']],
|
||||||
),
|
),
|
||||||
new Get(
|
new Get(
|
||||||
uriTemplate: '/building_cases/{id}/weights-report',
|
uriTemplate: '/building_cases/{id}/weights-report',
|
||||||
@@ -39,20 +39,20 @@ class BuildingCase
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['building:read'])]
|
#[Groups(['building:read', 'building_case:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['building:read'])]
|
#[Groups(['building:read', 'building_case:read'])]
|
||||||
#[SerializedName('caseNumber')]
|
#[SerializedName('caseNumber')]
|
||||||
private ?int $case_number = null;
|
private ?int $case_number = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
#[Groups(['building:read'])]
|
#[Groups(['building:read', 'building_case:read'])]
|
||||||
private ?string $code = null;
|
private ?string $code = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['building:read'])]
|
#[Groups(['building:read', 'building_case:read'])]
|
||||||
private ?int $capacity = null;
|
private ?int $capacity = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,16 +62,15 @@ class BuildingCase
|
|||||||
private Collection $id_case_position;
|
private Collection $id_case_position;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'buildingCases')]
|
#[ORM\ManyToOne(inversedBy: 'buildingCases')]
|
||||||
|
#[Groups(['building_case:read'])]
|
||||||
|
#[SerializedName('building')]
|
||||||
private ?Building $id_building = null;
|
private ?Building $id_building = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'id_case')]
|
|
||||||
#[Groups(['building:read'])]
|
|
||||||
private ?Statut $statut = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Bovine>
|
* @var Collection<int, Bovine>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: Bovine::class, mappedBy: 'buildingCase')]
|
#[ORM\OneToMany(targetEntity: Bovine::class, mappedBy: 'buildingCase')]
|
||||||
|
#[Groups(['building_case:read'])]
|
||||||
private Collection $bovines;
|
private Collection $bovines;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -170,16 +169,17 @@ class BuildingCase
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatut(): ?Statut
|
/**
|
||||||
|
* @return array{label: string, couleur: string}
|
||||||
|
*/
|
||||||
|
#[Groups(['building:read', 'building_case:read'])]
|
||||||
|
public function getStatut(): array
|
||||||
{
|
{
|
||||||
return $this->statut;
|
if ($this->bovines->count() > 0) {
|
||||||
}
|
return ['label' => 'Occupé', 'couleur' => '#3A506B'];
|
||||||
|
}
|
||||||
|
|
||||||
public function setStatut(?Statut $statut): static
|
return ['label' => 'Libre', 'couleur' => '#A3B18A'];
|
||||||
{
|
|
||||||
$this->statut = $statut;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Entity;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
|
||||||
use Doctrine\Common\Collections\Collection;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
|
||||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
|
||||||
|
|
||||||
#[ORM\Entity]
|
|
||||||
#[ApiResource(
|
|
||||||
operations: [
|
|
||||||
new Get(
|
|
||||||
requirements: ['id' => '\d+'],
|
|
||||||
normalizationContext: ['groups' => ['building:read']],
|
|
||||||
),
|
|
||||||
new GetCollection(
|
|
||||||
normalizationContext: ['groups' => ['building:read']],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
security: "is_granted('ROLE_USER')",
|
|
||||||
)]
|
|
||||||
class Statut
|
|
||||||
{
|
|
||||||
#[ORM\Id]
|
|
||||||
#[ORM\GeneratedValue]
|
|
||||||
#[ORM\Column]
|
|
||||||
#[Groups(['building:read'])]
|
|
||||||
private ?int $id = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
|
||||||
#[Groups(['building:read'])]
|
|
||||||
private ?string $label = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
|
||||||
#[Groups(['building:read'])]
|
|
||||||
private ?string $code = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
|
||||||
#[Groups(['building:read'])]
|
|
||||||
#[SerializedName('couleur')]
|
|
||||||
private ?string $color = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Collection<int, BuildingCase>
|
|
||||||
*/
|
|
||||||
#[ORM\OneToMany(targetEntity: BuildingCase::class, mappedBy: 'statut')]
|
|
||||||
private Collection $id_case;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->id_case = new ArrayCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setId(int $id): static
|
|
||||||
{
|
|
||||||
$this->id = $id;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLabel(): ?string
|
|
||||||
{
|
|
||||||
return $this->label;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setLabel(string $label): static
|
|
||||||
{
|
|
||||||
$this->label = $label;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCode(): ?string
|
|
||||||
{
|
|
||||||
return $this->code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCode(string $code): static
|
|
||||||
{
|
|
||||||
$this->code = $code;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getColor(): ?string
|
|
||||||
{
|
|
||||||
return $this->color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setColor(string $color): static
|
|
||||||
{
|
|
||||||
$this->color = $color;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, BuildingCase>
|
|
||||||
*/
|
|
||||||
public function getIdCase(): Collection
|
|
||||||
{
|
|
||||||
return $this->id_case;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addIdCase(BuildingCase $idCase): static
|
|
||||||
{
|
|
||||||
if (!$this->id_case->contains($idCase)) {
|
|
||||||
$this->id_case->add($idCase);
|
|
||||||
$idCase->setStatut($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function removeIdCase(BuildingCase $idCase): static
|
|
||||||
{
|
|
||||||
if ($this->id_case->removeElement($idCase)) {
|
|
||||||
// set the owning side to null (unless already changed)
|
|
||||||
if ($idCase->getStatut() === $this) {
|
|
||||||
$idCase->setStatut(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
68
src/State/BovineProcessor.php
Normal file
68
src/State/BovineProcessor.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\Entity\Bovine;
|
||||||
|
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class BovineProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BovinApiInterface $bovinApi,
|
||||||
|
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||||
|
private readonly ProcessorInterface $persistProcessor,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||||
|
{
|
||||||
|
if ($data instanceof Bovine && '' !== $data->getNationalNumber()) {
|
||||||
|
$this->enrichFromEdnotif($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function enrichFromEdnotif(Bovine $bovine): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$animalFile = $this->bovinApi->getAnimalFile(
|
||||||
|
nationalNumber: $bovine->getNationalNumber(),
|
||||||
|
countryCode: 'FR',
|
||||||
|
);
|
||||||
|
|
||||||
|
$identification = $animalFile->identification;
|
||||||
|
if (null === $identification) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bovine->setWorkNumber($identification->workNumber);
|
||||||
|
$bovine->setBirthDate($identification->birthDate?->date);
|
||||||
|
$bovine->setBreedCode($this->normalizeBreedCode($identification->breedType));
|
||||||
|
} catch (Throwable) {
|
||||||
|
// External service unavailable — persist bovine without enrichment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeBreedCode(mixed $breedType): ?string
|
||||||
|
{
|
||||||
|
if (null === $breedType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($breedType)) {
|
||||||
|
return (string) $breedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
|
||||||
|
return $matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,8 @@ use App\Entity\BuildingCase;
|
|||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Dompdf\Dompdf;
|
use Dompdf\Dompdf;
|
||||||
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use Throwable;
|
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Error\LoaderError;
|
use Twig\Error\LoaderError;
|
||||||
use Twig\Error\RuntimeError;
|
use Twig\Error\RuntimeError;
|
||||||
@@ -40,7 +38,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private BovinApiInterface $bovinApi,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,24 +65,9 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$workNumber = null;
|
$breedCode = $bovine->getBreedCode();
|
||||||
$birthDate = null;
|
if (null === $headerBreedCode && null !== $breedCode) {
|
||||||
$breedCode = null;
|
$headerBreedCode = $breedCode;
|
||||||
|
|
||||||
try {
|
|
||||||
$animalFileDto = $this->bovinApi->getAnimalFile(
|
|
||||||
nationalNumber: $bovine->getNationalNumber(),
|
|
||||||
countryCode: 'FR',
|
|
||||||
);
|
|
||||||
|
|
||||||
$workNumber = $animalFileDto->identification?->workNumber;
|
|
||||||
$birthDate = $animalFileDto->identification?->birthDate?->date?->format('d/m/y');
|
|
||||||
$breedCode = $this->normalizeBreedCode($animalFileDto->identification?->breedType);
|
|
||||||
if (null === $headerBreedCode && null !== $breedCode) {
|
|
||||||
$headerBreedCode = $breedCode;
|
|
||||||
}
|
|
||||||
} catch (Throwable) {
|
|
||||||
// Keep row data even if external identification service is unavailable.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$arrivalDate = $bovine->getArrivalDate();
|
$arrivalDate = $bovine->getArrivalDate();
|
||||||
@@ -101,8 +83,8 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
|||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'nationalNumber' => $bovine->getNationalNumber(),
|
'nationalNumber' => $bovine->getNationalNumber(),
|
||||||
'workNumber' => $workNumber,
|
'workNumber' => $bovine->getWorkNumber(),
|
||||||
'birthDate' => $birthDate,
|
'birthDate' => $bovine->getBirthDate()?->format('d/m/y'),
|
||||||
'receivedWeight' => $bovine->getReceivedWeight(),
|
'receivedWeight' => $bovine->getReceivedWeight(),
|
||||||
'arrivalDate' => $bovine->getArrivalDate()?->format('d/m/Y'),
|
'arrivalDate' => $bovine->getArrivalDate()?->format('d/m/Y'),
|
||||||
'projectedWeights' => $projectedWeights,
|
'projectedWeights' => $projectedWeights,
|
||||||
@@ -131,23 +113,6 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeBreedCode(mixed $breedType): ?string
|
|
||||||
{
|
|
||||||
if (null === $breedType) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_numeric($breedType)) {
|
|
||||||
return (string) $breedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($breedType) && preg_match('/\d+/', $breedType, $matches)) {
|
|
||||||
return $matches[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveDailyGainKg(?string $breedCode): float
|
private function resolveDailyGainKg(?string $breedCode): float
|
||||||
{
|
{
|
||||||
return 1.3;
|
return 1.3;
|
||||||
|
|||||||
@@ -265,23 +265,15 @@
|
|||||||
TABLEAU PRINCIPAL
|
TABLEAU PRINCIPAL
|
||||||
========================= -->
|
========================= -->
|
||||||
<table class="main">
|
<table class="main">
|
||||||
<colgroup>
|
|
||||||
<col style="width:8%">
|
|
||||||
<col style="width:4%">
|
|
||||||
<col style="width:7%">
|
|
||||||
{% for month in monthHeaders %}
|
|
||||||
<col style="width:6.75%">
|
|
||||||
{% endfor %}
|
|
||||||
</colgroup>
|
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="4" class="head-big">N° de<br>travail</th>
|
<th rowspan="4" class="head-big" style="width:5%">N° de<br>travail</th>
|
||||||
<th rowspan="4" class="head-big head-big-weight">Poids<br>(kg)</th>
|
<th rowspan="4" class="head-big" style="width:5%">N° de<br>travail</th>
|
||||||
<th rowspan="4" class="head-big">Date de<br>naissance</th>
|
<th rowspan="4" class="head-big head-big-weight" style="width:4%">Poids<br>(kg)</th>
|
||||||
|
<th rowspan="4" class="head-big" style="width:7%">Date de<br>naissance</th>
|
||||||
|
|
||||||
{% for month in monthHeaders|default([]) %}
|
{% for month in monthHeaders|default([]) %}
|
||||||
<th class="month">{{ month.name }}</th>
|
<th class="month" style="width:6.58%">{{ month.name }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -315,6 +307,7 @@
|
|||||||
{% set baseWeight = row ? (row.receivedWeight ?? null) : null %}
|
{% set baseWeight = row ? (row.receivedWeight ?? null) : null %}
|
||||||
|
|
||||||
<tr class="data-row">
|
<tr class="data-row">
|
||||||
|
<td class="row-work"></td>
|
||||||
<td class="row-work">{{ row ? (row.workNumber ?? '') : '' }}</td>
|
<td class="row-work">{{ row ? (row.workNumber ?? '') : '' }}</td>
|
||||||
<td class="row-weight">{{ baseWeight ?? '' }}</td>
|
<td class="row-weight">{{ baseWeight ?? '' }}</td>
|
||||||
<td class="row-birth">
|
<td class="row-birth">
|
||||||
|
|||||||
Reference in New Issue
Block a user