Compare commits
23 Commits
v0.0.93
...
b390dd51de
| Author | SHA1 | Date | |
|---|---|---|---|
| b390dd51de | |||
| 754898da39 | |||
| 5b24d642bb | |||
|
|
cde2c4fbb7 | ||
| 5552d98935 | |||
| b932798a87 | |||
| ee766311e3 | |||
| 2f8aa1dd32 | |||
| de76a77120 | |||
| 642ee43c53 | |||
|
|
9e67a5e289 | ||
| 92289f9cb2 | |||
|
|
59418f2c66 | ||
| e1c9e25187 | |||
| 0b22574932 | |||
|
|
9115699f96 | ||
| 178b4e4eee | |||
|
|
fbc8365405 | ||
| 4561467532 | |||
| c441420522 | |||
| ba9ea2de71 | |||
|
|
961fa63f3d | ||
| bebfabcacc |
@@ -4,7 +4,9 @@
|
||||
"Bash(npm run:*)",
|
||||
"WebFetch(domain:geo.api.gouv.fr)",
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(python3 -c \":*)"
|
||||
"Bash(python3 -c \":*)",
|
||||
"Bash(make cache-clear *)",
|
||||
"Bash(make test *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
10
.idea/db-forest-config.xml
generated
10
.idea/db-forest-config.xml
generated
@@ -1,5 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-forest-configuration">
|
||||
<data version="2">.
|
||||
----------------------------------------
|
||||
1:0:9cad43df-2147-4989-b7a4-443067034884
|
||||
2:0:ae622167-c834-4e7b-87a5-c1721036f5dc
|
||||
3:0:f407a514-c6b4-4b26-9555-445a85892502
|
||||
4:0:09e221b8-067a-488b-9c1d-4e155a333079
|
||||
5:0:9d8c1ad3-2491-4642-964a-666003c14128
|
||||
.</data>
|
||||
</component>
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:9cad43df-2147-4989-b7a4-443067034884 4:0:09e221b8-067a-488b-9c1d-4e155a333079 " />
|
||||
</component>
|
||||
|
||||
5
.idea/ferme.iml
generated
5
.idea/ferme.iml
generated
@@ -155,6 +155,11 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/maennchen/zipstream-php" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/complex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/markbaker/matrix" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
|
||||
<excludePattern pattern="reference.php" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
5
.idea/php.xml
generated
5
.idea/php.xml
generated
@@ -174,6 +174,11 @@
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
|
||||
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||
|
||||
145
.idea/workspace.xml
generated
145
.idea/workspace.xml
generated
@@ -4,12 +4,16 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : label age bovin">
|
||||
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/db-forest-config.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/db-forest-config.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/ferme.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/ferme.iml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/php.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/php.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/waiting-reception.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/waiting-reception.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/shipment/waiting-shipment.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/shipment/waiting-shipment.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/bovine/[id].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/bovine/[id].vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/BovineMovement.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/BovineMovement.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/State/Bovin/BovineMovementProcessor.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/State/Bovin/BovineMovementProcessor.php" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -41,7 +45,7 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="feature/FER-13-faire-des-recherches-sur-le-scanner-des-betes" />
|
||||
<entry key="$PROJECT_DIR$" value="feat/entree-sortie" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@@ -213,6 +217,11 @@
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
|
||||
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
@@ -232,7 +241,9 @@
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "fix/FER-15-fix-droit-de-suppression-reception-expedition-util",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feat/vie-du-bovin",
|
||||
"git.auto.fetch.suggestion.counter": "3",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/Ferme",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
@@ -274,7 +285,7 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.32098.40" />
|
||||
<option value="bundled-php-predefined-a98d8de5180a-022fa7b8ab75-com.jetbrains.php.sharedIndexes-PS-261.23567.149" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -327,54 +338,16 @@
|
||||
<workItem from="1773824491213" duration="24805000" />
|
||||
<workItem from="1774275549972" duration="51000" />
|
||||
<workItem from="1774276665015" duration="33750000" />
|
||||
</task>
|
||||
<task id="LOCAL-00037" summary="feat : finalisation de l'étape 1 "Réception" (formulaire)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769529522614</created>
|
||||
<option name="number" value="00037" />
|
||||
<option name="presentableId" value="LOCAL-00037" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769529522614</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00038" summary="feat : ajout du numéro identification des receptions et ajustement du bon de reception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769676223697</created>
|
||||
<option name="number" value="00038" />
|
||||
<option name="presentableId" value="LOCAL-00038" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769676223697</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00039" summary="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769700808988</created>
|
||||
<option name="number" value="00039" />
|
||||
<option name="presentableId" value="LOCAL-00039" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769700808988</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00040" summary="feat : mise en place de composant UI pour les select, checkbox, date, text">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769705141157</created>
|
||||
<option name="number" value="00040" />
|
||||
<option name="presentableId" value="LOCAL-00040" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769705141157</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00041" summary="feat : update CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769705240487</created>
|
||||
<option name="number" value="00041" />
|
||||
<option name="presentableId" value="LOCAL-00041" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769705240487</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00042" summary="feat : ajout de commentaire">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769760766200</created>
|
||||
<option name="number" value="00042" />
|
||||
<option name="presentableId" value="LOCAL-00042" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769760766200</updated>
|
||||
<workItem from="1776755742205" duration="88521000" />
|
||||
<workItem from="1777453284124" duration="86000" />
|
||||
<workItem from="1777453433907" duration="337000" />
|
||||
<workItem from="1777454070632" duration="17254000" />
|
||||
<workItem from="1777540415843" duration="13205000" />
|
||||
<workItem from="1777877316149" duration="29389000" />
|
||||
<workItem from="1777982616362" duration="23909000" />
|
||||
<workItem from="1778482021120" duration="1280000" />
|
||||
<workItem from="1778656317630" duration="279000" />
|
||||
<workItem from="1778664396844" duration="2576000" />
|
||||
</task>
|
||||
<task id="LOCAL-00043" summary="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception">
|
||||
<option name="closed" value="true" />
|
||||
@@ -720,7 +693,55 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774543840891</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="86" />
|
||||
<task id="LOCAL-00086" summary="fix : update icon entrée/sortie">
|
||||
<option name="closed" value="true" />
|
||||
<created>1777896558092</created>
|
||||
<option name="number" value="00086" />
|
||||
<option name="presentableId" value="LOCAL-00086" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1777896558092</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00087" summary="fix : wording">
|
||||
<option name="closed" value="true" />
|
||||
<created>1777983048277</created>
|
||||
<option name="number" value="00087" />
|
||||
<option name="presentableId" value="LOCAL-00087" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1777983048278</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00088" summary="fix : wording">
|
||||
<option name="closed" value="true" />
|
||||
<created>1777983581324</created>
|
||||
<option name="number" value="00088" />
|
||||
<option name="presentableId" value="LOCAL-00088" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1777983581324</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00089" summary="feat : update CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1778073247660</created>
|
||||
<option name="number" value="00089" />
|
||||
<option name="presentableId" value="LOCAL-00089" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1778073247660</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00090" summary="feat : amélioration du tableau bovin">
|
||||
<option name="closed" value="true" />
|
||||
<created>1778135981350</created>
|
||||
<option name="number" value="00090" />
|
||||
<option name="presentableId" value="LOCAL-00090" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1778135981350</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00091" summary="fix : label age bovin">
|
||||
<option name="closed" value="true" />
|
||||
<created>1778136373027</created>
|
||||
<option name="number" value="00091" />
|
||||
<option name="presentableId" value="LOCAL-00091" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1778136373027</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="92" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -770,10 +791,6 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="feat : changelog update" />
|
||||
<MESSAGE value="fix : color tab" />
|
||||
<MESSAGE value="feat : modification front de la page admin transporteur" />
|
||||
<MESSAGE value="fix : espacement et changelog" />
|
||||
<MESSAGE value="fix : espacement" />
|
||||
<MESSAGE value="fix : text" />
|
||||
<MESSAGE value="feat : front page admin bovin et changelog" />
|
||||
@@ -792,10 +809,14 @@
|
||||
<MESSAGE value="feat : système de blocage utilisateur" />
|
||||
<MESSAGE value="feat : ajout d'un système de scanner bovin" />
|
||||
<MESSAGE value="feat : mise à jour du CLAUDE.md" />
|
||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
||||
<MESSAGE value="feat : la page de scanner est accessible que pour les admins" />
|
||||
<MESSAGE value="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente" />
|
||||
<MESSAGE value="fix : update icon entrée/sortie" />
|
||||
<MESSAGE value="fix : wording" />
|
||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
||||
<MESSAGE value="feat : amélioration du tableau bovin" />
|
||||
<MESSAGE value="fix : label age bovin" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix : label age bovin" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
@@ -65,6 +65,7 @@ Ajouter dans le fichier .env du frontend
|
||||
* [#FER-15] Les non-admin ne peuvent plus supprimer de réception/expédition en attente
|
||||
* [#FER-17] Ecran d'ajout de bovin
|
||||
* [#FER-18] Mise à jour du tableau d'arrivage
|
||||
* [#FER-26] Passeport du bovin
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.0.93'
|
||||
app.version: '0.0.99'
|
||||
|
||||
35
frontend/components/ui/UiTabs.vue
Normal file
35
frontend/components/ui/UiTabs.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="font-bold text-3xl uppercase px-12 cursor-pointer"
|
||||
:class="[
|
||||
modelValue === tab.key
|
||||
? 'border-b-[6px] border-primary-500 text-primary-500'
|
||||
: 'text-primary-500/50',
|
||||
tab.error ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="emit('update:modelValue', tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends string">
|
||||
export interface UiTab<K extends string = string> {
|
||||
key: K
|
||||
label: string
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
modelValue: T
|
||||
tabs: UiTab<T>[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: T): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -32,6 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Type de bovin' })
|
||||
|
||||
import {createBovin, getBovin, updateBovin} from "~/services/bovine-type";
|
||||
import type {BovineTypeData, BovinFormData} from "~/services/dto/bovine-type-data";
|
||||
const router = useRouter()
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Types de bovins' })
|
||||
|
||||
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Transporteur' })
|
||||
|
||||
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
||||
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
||||
import {computed} from "vue";
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Transporteurs' })
|
||||
|
||||
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Client' })
|
||||
|
||||
import {computed, reactive, ref, watch} from "vue"
|
||||
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Adresse client' })
|
||||
|
||||
import type { AddressData, AddressPayload } from "~/services/address"
|
||||
import { createAddress, getAddress, updateAddress } from "~/services/address"
|
||||
import { getCustomer, updateCustomer } from "~/services/customer"
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Clients' })
|
||||
|
||||
import type { CustomerData } from '~/services/dto/customer-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Fournisseur' })
|
||||
|
||||
import {computed, reactive, ref, watch} from "vue"
|
||||
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
||||
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Adresse fournisseur' })
|
||||
|
||||
import type {AddressData, AddressPayload} from "~/services/address";
|
||||
import {createAddress, getAddress, updateAddress} from "~/services/address";
|
||||
import {getSupplier, updateSupplier} from "~/services/supplier";
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Fournisseurs' })
|
||||
|
||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Utilisateur' })
|
||||
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ROLE } from '~/utils/constants'
|
||||
import { createUser, updateUser, getUser } from '~/services/auth'
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Utilisateurs' })
|
||||
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { ROLE } from '~/utils/constants'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
359
frontend/pages/bovine/[id].vue
Normal file
359
frontend/pages/bovine/[id].vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="px-[86px]">
|
||||
<div class="flex items-center justify-between relative mb-10">
|
||||
<div class="flex flex-row absolute -left-[60px]">
|
||||
<Icon
|
||||
@click="goBack"
|
||||
name="gg:arrow-left-o"
|
||||
size="44"
|
||||
class="cursor-pointer text-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="font-bold text-3xl uppercase text-primary-500">Vie du bovin</h1>
|
||||
</div>
|
||||
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="tabs"
|
||||
/>
|
||||
|
||||
<div v-if="auth.isBureau" v-show="activeTab === 'mouvement'">
|
||||
<form :class="{ submitted: movementSubmitted }" @submit.prevent="submitMovement">
|
||||
<div class="flex flex-cols-3 justify-between mb-10">
|
||||
<UiSelect
|
||||
id="movement-building"
|
||||
v-model="newMovementBuildingId"
|
||||
label="Bâtiment"
|
||||
:options="buildingOptions"
|
||||
wrapper-class="w-[280px]"
|
||||
required
|
||||
/>
|
||||
<UiSelect
|
||||
id="movement-case"
|
||||
v-model="newMovementCaseId"
|
||||
label="Case"
|
||||
:options="caseOptions"
|
||||
:disabled="!newMovementBuildingId"
|
||||
wrapper-class="w-[280px]"
|
||||
required
|
||||
/>
|
||||
<UiDateInput
|
||||
id="movement-date"
|
||||
v-model="newMovementDate"
|
||||
label="Date mouvement"
|
||||
wrapper-class="w-[280px]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center mb-11">
|
||||
<UiButton
|
||||
type="submit"
|
||||
class="inline-flex items-center justify-center gap-2 text-xl text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80"
|
||||
:disabled="isSubmittingMovement"
|
||||
:loading="isSubmittingMovement"
|
||||
@click="movementSubmitted = true"
|
||||
>
|
||||
<Icon name="mdi:plus" size="28" />
|
||||
Ajouter
|
||||
</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<UiDataTable
|
||||
:columns="movementColumns"
|
||||
:items="filteredMovementRows"
|
||||
:per-page="10"
|
||||
>
|
||||
<template #header-building>
|
||||
<UiTextInput
|
||||
v-model="movementFilters.building"
|
||||
placeholder="Bâtiment"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
<template #header-case>
|
||||
<UiTextInput
|
||||
v-model="movementFilters.case"
|
||||
placeholder="Case"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
<template #header-enteredAt>
|
||||
<UiTextInput :model-value="''" placeholder="Du" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-leftAt>
|
||||
<UiTextInput :model-value="''" placeholder="Au" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-duration>
|
||||
<UiTextInput :model-value="''" placeholder="Durée" size="compact" disabled />
|
||||
</template>
|
||||
<template #cell-leftAt="{ item }">
|
||||
<span v-if="item.leftAt">{{ item.leftAt }}</span>
|
||||
<span v-else class="italic text-slate-500">En cours</span>
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'passeport'">
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Veau</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Sexe</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-b border-black px-2 py-1 text-center font-semibold text-sm">Date de naissance</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.nationalNumber) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.workNumber) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.sex) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.bovineType?.code) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.bovineType?.label) }}</div>
|
||||
<div class="px-2 py-1 text-center">{{ formatDate(bovine?.birthDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-9">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Père</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="col-span-2 border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="col-span-2 border-b border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.fatherNationalNumber) }}</div>
|
||||
<div class="col-span-2 border-r border-black px-2 py-1 text-center">{{ display(workNumberFromNational(bovine?.fatherNationalNumber)) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.fatherBovineType?.code) }}</div>
|
||||
<div class="col-span-2 px-2 py-1 text-center">{{ display(bovine?.fatherBovineType?.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-9">
|
||||
<div class="grid grid-cols-[3rem_repeat(6,minmax(0,1fr))] grid-rows-2 border-2 border-black">
|
||||
<div class="row-span-2 flex items-center justify-center border-r-2 border-black">
|
||||
<span class="uppercase font-bold -rotate-90 whitespace-nowrap transform-gpu">Mère</span>
|
||||
</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Numéro national</div>
|
||||
<div class="col-span-2 border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">N° de travail</div>
|
||||
<div class="border-b border-r border-black px-2 py-1 text-center font-semibold text-sm">Code race</div>
|
||||
<div class="col-span-2 border-b border-black px-2 py-1 text-center font-semibold text-sm">Type de race</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.motherNationalNumber) }}</div>
|
||||
<div class="col-span-2 border-r border-black px-2 py-1 text-center">{{ display(workNumberFromNational(bovine?.motherNationalNumber)) }}</div>
|
||||
<div class="border-r border-black px-2 py-1 text-center">{{ display(bovine?.motherBovineType?.code) }}</div>
|
||||
<div class="col-span-2 px-2 py-1 text-center">{{ display(bovine?.motherBovineType?.label) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'sante'">
|
||||
<div class="border-2 border-dashed border-primary-500 rounded-md py-16 text-center text-primary-500 font-bold uppercase text-2xl">
|
||||
À venir
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBuildingList } from '~/services/building'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
useHead({ title: 'Vie du bovin' })
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
type BovineTab = 'mouvement' | 'passeport' | 'sante'
|
||||
const tabs = computed(() => [
|
||||
...(auth.isBureau ? [{ key: 'mouvement' as const, label: 'Mouvement' }] : []),
|
||||
{ key: 'passeport' as const, label: 'Passeport bovin' },
|
||||
{ key: 'sante' as const, label: 'Santé' }
|
||||
])
|
||||
const activeTab = ref<BovineTab>(auth.isBureau ? 'mouvement' : 'passeport')
|
||||
|
||||
interface BovineTypeRef {
|
||||
id: number
|
||||
label: string | null
|
||||
code: string | null
|
||||
}
|
||||
|
||||
interface BuildingRef {
|
||||
label: string | null
|
||||
}
|
||||
|
||||
interface BuildingCaseRef {
|
||||
caseNumber: number | null
|
||||
building: BuildingRef | null
|
||||
}
|
||||
|
||||
interface BovineMovementData {
|
||||
id: number
|
||||
enteredAt: string
|
||||
leftAt: string | null
|
||||
buildingCase: BuildingCaseRef | null
|
||||
building: BuildingRef | null
|
||||
}
|
||||
|
||||
interface BovinePassportData {
|
||||
id: number
|
||||
nationalNumber: string
|
||||
workNumber: string | null
|
||||
sex: string | null
|
||||
birthDate: string | null
|
||||
exitedAt: string | null
|
||||
exitDate: string | null
|
||||
bovineType: BovineTypeRef | null
|
||||
motherNationalNumber: string | null
|
||||
motherBovineType: BovineTypeRef | null
|
||||
fatherNationalNumber: string | null
|
||||
fatherBovineType: BovineTypeRef | null
|
||||
movements: BovineMovementData[]
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const api = useApi()
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.state?.back) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/inventory')
|
||||
}
|
||||
}
|
||||
|
||||
const todayIso = () => new Date().toISOString().slice(0, 10)
|
||||
|
||||
const bovine = ref<BovinePassportData | null>(null)
|
||||
const buildings = ref<BuildingData[]>([])
|
||||
const newMovementBuildingId = ref<string | number | null>(null)
|
||||
const newMovementCaseId = ref<string | number | null>(null)
|
||||
const newMovementDate = ref<string>(todayIso())
|
||||
const isSubmittingMovement = ref(false)
|
||||
const movementSubmitted = ref(false)
|
||||
const movementFilters = ref({ building: '', case: '' })
|
||||
|
||||
const bovineId = computed(() => {
|
||||
const raw = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||
const n = Number(raw)
|
||||
return Number.isFinite(n) ? n : null
|
||||
})
|
||||
|
||||
const display = (value: string | null | undefined) => (value && value !== '' ? value : '—')
|
||||
|
||||
const workNumberFromNational = (nationalNumber: string | null | undefined) => {
|
||||
if (!nationalNumber) return null
|
||||
return nationalNumber.slice(-4)
|
||||
}
|
||||
|
||||
const formatDate = (date: string | null | undefined) => {
|
||||
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 buildingOptions = computed(() =>
|
||||
buildings.value.map(b => ({ value: b.id, label: b.label }))
|
||||
)
|
||||
|
||||
const caseOptions = computed(() => {
|
||||
const building = buildings.value.find(b => b.id === Number(newMovementBuildingId.value))
|
||||
if (!building?.buildingCases) return []
|
||||
return [...building.buildingCases]
|
||||
.sort((a, b) => (a.caseNumber ?? 0) - (b.caseNumber ?? 0))
|
||||
.map(c => ({
|
||||
value: c.id,
|
||||
label: `Case ${c.caseNumber ?? c.code ?? c.id}`
|
||||
}))
|
||||
})
|
||||
|
||||
watch(newMovementBuildingId, () => {
|
||||
newMovementCaseId.value = null
|
||||
})
|
||||
|
||||
const movementColumns = [
|
||||
{ key: 'building', label: 'Bâtiment' },
|
||||
{ key: 'case', label: 'Case' },
|
||||
{ key: 'enteredAt', label: 'Du' },
|
||||
{ key: 'leftAt', label: 'Au' },
|
||||
{ key: 'duration', label: 'Durée' }
|
||||
]
|
||||
|
||||
const movementEndDate = (movement: BovineMovementData): string | null => {
|
||||
return movement.leftAt ?? bovine.value?.exitedAt ?? bovine.value?.exitDate ?? null
|
||||
}
|
||||
|
||||
const formatDuration = (movement: BovineMovementData): string => {
|
||||
const start = new Date(movement.enteredAt)
|
||||
if (isNaN(start.getTime())) return '—'
|
||||
const endRaw = movementEndDate(movement)
|
||||
const end = endRaw ? new Date(endRaw) : new Date()
|
||||
if (isNaN(end.getTime())) return '—'
|
||||
const days = Math.max(0, Math.floor((end.getTime() - start.getTime()) / 86_400_000))
|
||||
return `${days} j`
|
||||
}
|
||||
|
||||
const movementRows = computed(() => {
|
||||
const list = bovine.value?.movements ?? []
|
||||
return list.map(m => ({
|
||||
id: m.id,
|
||||
building: m.buildingCase?.building?.label ?? m.building?.label ?? '—',
|
||||
case: m.buildingCase?.caseNumber != null ? `Case ${m.buildingCase.caseNumber}` : '—',
|
||||
enteredAt: formatDate(m.enteredAt),
|
||||
leftAt: m.leftAt ? formatDate(m.leftAt) : null,
|
||||
duration: formatDuration(m)
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredMovementRows = computed(() => {
|
||||
const buildingFilter = movementFilters.value.building.trim().toLowerCase()
|
||||
const caseFilter = movementFilters.value.case.trim().toLowerCase()
|
||||
return movementRows.value.filter(row => {
|
||||
if (buildingFilter && !row.building.toLowerCase().includes(buildingFilter)) return false
|
||||
if (caseFilter && !row.case.toLowerCase().includes(caseFilter)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const submitMovement = async () => {
|
||||
if (!newMovementCaseId.value || !newMovementDate.value || bovineId.value === null) return
|
||||
|
||||
const buildingLabel = buildingOptions.value.find(o => o.value === Number(newMovementBuildingId.value))?.label ?? '—'
|
||||
const caseLabel = caseOptions.value.find(o => o.value === Number(newMovementCaseId.value))?.label ?? '—'
|
||||
const dateLabel = formatDate(newMovementDate.value)
|
||||
const confirmed = window.confirm(
|
||||
`Confirmer la création du mouvement ?\n\nBâtiment : ${buildingLabel}\nCase : ${caseLabel}\nDate : ${dateLabel}`
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
isSubmittingMovement.value = true
|
||||
try {
|
||||
await api.post('bovine_movements', {
|
||||
bovine: `/api/bovines/${bovineId.value}`,
|
||||
buildingCase: `/api/building_cases/${newMovementCaseId.value}`,
|
||||
enteredAt: newMovementDate.value
|
||||
}, { toastSuccessMessage: 'Mouvement enregistré' })
|
||||
bovine.value = await api.get<BovinePassportData>(`bovines/${bovineId.value}`)
|
||||
newMovementBuildingId.value = null
|
||||
newMovementCaseId.value = null
|
||||
newMovementDate.value = todayIso()
|
||||
movementSubmitted.value = false
|
||||
} finally {
|
||||
isSubmittingMovement.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (bovineId.value === null) return
|
||||
const [bovineData, buildingList] = await Promise.all([
|
||||
api.get<BovinePassportData>(`bovines/${bovineId.value}`),
|
||||
getBuildingList()
|
||||
])
|
||||
bovine.value = bovineData
|
||||
buildings.value = buildingList
|
||||
})
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Accueil' })
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-wrap justify-center pb-16 gap-12">
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
<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>
|
||||
@@ -80,6 +80,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Bâtiments' })
|
||||
|
||||
import type {BuildingData} from "~/services/dto/building-data"
|
||||
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
|
||||
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
|
||||
|
||||
@@ -23,14 +23,6 @@
|
||||
<Icon name="mdi:printer-outline" size="32" class="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-if="hasCaseId && auth.isAdmin"
|
||||
: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"
|
||||
>
|
||||
<Icon name="mdi:plus" size="28" />
|
||||
Ajouter
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
@@ -56,7 +48,7 @@
|
||||
:items="items"
|
||||
:total-items="totalItems"
|
||||
:loading="loading"
|
||||
:row-clickable="auth.isAdmin"
|
||||
row-clickable
|
||||
empty-message="Aucun bovin dans cette case."
|
||||
@row-click="goToBovine"
|
||||
>
|
||||
@@ -130,9 +122,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Cases' })
|
||||
|
||||
import type { BuildingCaseData } from '~/services/dto/building-case-data'
|
||||
import type { BovineData } from '~/services/dto/bovine-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
import { useBovineColumns } from '~/composables/useBovineColumns'
|
||||
import { formatAgeLabel, ageBadgeClass } from '~/utils/bovine-age'
|
||||
@@ -141,7 +134,6 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { printPdf } = usePdfPrinter()
|
||||
const api = useApi()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const caseId = computed(() => Number(route.query.id))
|
||||
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
|
||||
@@ -231,11 +223,6 @@ const title = computed(() => {
|
||||
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)
|
||||
@@ -268,11 +255,7 @@ const printCaseReport = async () => {
|
||||
}
|
||||
|
||||
const goToBovine = (bovine: BovineData) => {
|
||||
if (!auth.isAdmin) return
|
||||
router.push({
|
||||
path: '/infrastructure/bovine',
|
||||
query: { id: String(bovine.id), caseId: String(caseId.value) }
|
||||
})
|
||||
router.push(`/bovine/${bovine.id}`)
|
||||
}
|
||||
|
||||
watch(caseId, (id) => {
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
:items="items"
|
||||
:total-items="totalItems"
|
||||
:loading="loading"
|
||||
row-clickable
|
||||
@row-click="(item: BovineData) => router.push(`/bovine/${item.id}`)"
|
||||
>
|
||||
<template #header-nationalNumber>
|
||||
<UiTextInput
|
||||
@@ -147,6 +149,8 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Inventaire' })
|
||||
|
||||
import type { BovineData } from '~/services/dto/bovine-data'
|
||||
import type { InventoryExportFilters } from '~/components/inventory/inventory-export-modal.vue'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Connexion' })
|
||||
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { getUsers } from '~/services/auth'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Réception' })
|
||||
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWorkflowSteps } from '~/composables/useWorkflowSteps'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-start gap-10">
|
||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions finie</h1>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des réceptions finies</h1>
|
||||
</div>
|
||||
|
||||
<div class="px-[86px]">
|
||||
@@ -73,6 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Validation réception' })
|
||||
|
||||
import type { ReceptionData } from '~/services/dto/reception-data'
|
||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||
import { getReceptionTypeList } from '~/services/reception-type'
|
||||
|
||||
@@ -145,38 +145,14 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="formIsLoading">
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
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="[
|
||||
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weightsEmpty'"
|
||||
>
|
||||
pesée à vide
|
||||
</h1>
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasMerchandiseTabError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'merchandise'"
|
||||
>
|
||||
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||
</h1>
|
||||
</div>
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="[
|
||||
{ key: 'weights', label: 'pesée à plein', error: hasGrossWeightError },
|
||||
{ key: 'weightsEmpty', label: 'pesée à vide', error: hasTareWeightError },
|
||||
{ key: 'merchandise', label: isMerchandise ? 'Marchandise' : 'Bovins', error: hasMerchandiseTabError }
|
||||
]"
|
||||
/>
|
||||
<div class="mb-12 ">
|
||||
<update-weight
|
||||
v-show="activeTab === 'weights'"
|
||||
@@ -226,6 +202,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Modifier réception' })
|
||||
|
||||
import { usePdfPrinter } from '#imports'
|
||||
import { computed } from 'vue'
|
||||
import UpdateBovin from '~/components/reception/update-bovin.vue'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-start gap-10">
|
||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des réceptions en attente</h1>
|
||||
</div>
|
||||
|
||||
<div class="px-[86px]">
|
||||
@@ -72,6 +72,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Réceptions en attente' })
|
||||
|
||||
import type { ReceptionData } from '~/services/dto/reception-data'
|
||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||
import { deleteReception } from '~/services/reception'
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Scanner' })
|
||||
|
||||
import { ref, computed, nextTick, onMounted, watch } from 'vue'
|
||||
import { useBarcodeScanner } from '~/composables/useBarcodeScanner'
|
||||
import { createBovine } from '~/services/bovine'
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Expédition' })
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useShipmentStore } from '~/stores/shipment'
|
||||
import { useWorkflowSteps } from '~/composables/useWorkflowSteps'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-start gap-10">
|
||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions finie</h1>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des expéditions finies</h1>
|
||||
</div>
|
||||
|
||||
<div class="px-[86px]">
|
||||
@@ -71,6 +71,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Validation expédition' })
|
||||
|
||||
import type { ShipmentData } from '~/services/dto/shipment-data'
|
||||
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||
|
||||
@@ -146,28 +146,13 @@
|
||||
</div>
|
||||
|
||||
<div v-if="formIsLoading">
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||
:class="[
|
||||
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||
]"
|
||||
@click="activeTab = 'weightsEmpty'"
|
||||
>
|
||||
pesée à vide
|
||||
</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>
|
||||
<UiTabs
|
||||
v-model="activeTab"
|
||||
:tabs="[
|
||||
{ key: 'weightsEmpty', label: 'pesée à vide', error: hasTareWeightError },
|
||||
{ key: 'weights', label: 'pesée à plein', error: hasGrossWeightError }
|
||||
]"
|
||||
/>
|
||||
<div class="mb-12">
|
||||
<update-weight
|
||||
v-show="activeTab === 'weights'"
|
||||
@@ -197,6 +182,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Modifier expédition' })
|
||||
|
||||
import { usePdfPrinter } from '#imports'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import UpdateWeight from '~/components/commun/update-weight.vue'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-start gap-10">
|
||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
|
||||
<h1 class="text-3xl font-bold uppercase text-primary-500">liste des expéditions en attente</h1>
|
||||
</div>
|
||||
|
||||
<div class="px-[86px]">
|
||||
@@ -84,6 +84,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({ title: 'Expéditions en attente' })
|
||||
|
||||
import type { ShipmentData } from '~/services/dto/shipment-data'
|
||||
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||
import { deleteShipment } from '~/services/shipment'
|
||||
|
||||
@@ -4,7 +4,7 @@ export const formatAgeLabel = (months: number | null | undefined): string => {
|
||||
const remaining = months % 12
|
||||
let label = ''
|
||||
if (years > 0) label = `${years} an${years > 1 ? 's' : ''}`
|
||||
if (remaining > 0) label += `${label ? ' ' : ''}${remaining} mois`
|
||||
if (remaining > 0) label += `${label ? ' ' : ''}${remaining} m`
|
||||
if (!label) label = '< 1 mois'
|
||||
return label
|
||||
}
|
||||
|
||||
40
migrations/Version20260504125011.php
Normal file
40
migrations/Version20260504125011.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260504125011 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add mother/father national number and bovine type to bovine.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE bovine ADD mother_national_number VARCHAR(50) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE bovine ADD father_national_number VARCHAR(50) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE bovine ADD mother_bovine_type_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE bovine ADD father_bovine_type_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE bovine ADD CONSTRAINT FK_2068337F14E5E9FB FOREIGN KEY (mother_bovine_type_id) REFERENCES bovine_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE bovine ADD CONSTRAINT FK_2068337F53F12909 FOREIGN KEY (father_bovine_type_id) REFERENCES bovine_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_2068337F14E5E9FB ON bovine (mother_bovine_type_id)');
|
||||
$this->addSql('CREATE INDEX IDX_2068337F53F12909 ON bovine (father_bovine_type_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE bovine DROP CONSTRAINT FK_2068337F14E5E9FB');
|
||||
$this->addSql('ALTER TABLE bovine DROP CONSTRAINT FK_2068337F53F12909');
|
||||
$this->addSql('DROP INDEX IDX_2068337F14E5E9FB');
|
||||
$this->addSql('DROP INDEX IDX_2068337F53F12909');
|
||||
$this->addSql('ALTER TABLE bovine DROP mother_national_number');
|
||||
$this->addSql('ALTER TABLE bovine DROP father_national_number');
|
||||
$this->addSql('ALTER TABLE bovine DROP mother_bovine_type_id');
|
||||
$this->addSql('ALTER TABLE bovine DROP father_bovine_type_id');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20260506141455.php
Normal file
33
migrations/Version20260506141455.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260506141455 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create bovine_movement table to track internal building/case history.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE bovine_movement (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, bovine_id INT NOT NULL, building_case_id INT DEFAULT NULL, building_id INT DEFAULT NULL, entered_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, left_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX idx_bovine_movement_bovine ON bovine_movement (bovine_id)');
|
||||
$this->addSql('CREATE INDEX idx_bovine_movement_timeline ON bovine_movement (bovine_id, entered_at)');
|
||||
$this->addSql('CREATE INDEX idx_bovine_movement_case ON bovine_movement (building_case_id)');
|
||||
$this->addSql('CREATE INDEX idx_bovine_movement_building ON bovine_movement (building_id)');
|
||||
$this->addSql('ALTER TABLE bovine_movement ADD CONSTRAINT fk_bovine_movement_bovine FOREIGN KEY (bovine_id) REFERENCES bovine (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE bovine_movement ADD CONSTRAINT fk_bovine_movement_case FOREIGN KEY (building_case_id) REFERENCES building_case (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE bovine_movement ADD CONSTRAINT fk_bovine_movement_building FOREIGN KEY (building_id) REFERENCES building (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE bovine_movement');
|
||||
}
|
||||
}
|
||||
93
src/Command/BackfillBovineMovementsCommand.php
Normal file
93
src/Command/BackfillBovineMovementsCommand.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Bovine;
|
||||
use App\Entity\BovineMovement;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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 function count;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:backfill-bovine-movements',
|
||||
description: 'Crée un mouvement initial pour chaque bovin ayant une case ou un bâtiment mais aucun mouvement enregistré.'
|
||||
)]
|
||||
class BackfillBovineMovementsCommand extends Command
|
||||
{
|
||||
private const FLUSH_EVERY = 100;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$bovines = $this->entityManager->createQueryBuilder()
|
||||
->select('b')
|
||||
->from(Bovine::class, 'b')
|
||||
->where('b.buildingCase IS NOT NULL OR b.building IS NOT NULL')
|
||||
->andWhere('NOT EXISTS (SELECT 1 FROM '.BovineMovement::class.' m WHERE m.bovine = b)')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
$total = count($bovines);
|
||||
if (0 === $total) {
|
||||
$io->success('Aucun bovin à backfiller.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('%d bovin(s) à backfiller.', $total));
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
$created = 0;
|
||||
$fallback = 0;
|
||||
|
||||
foreach ($bovines as $i => $bovine) {
|
||||
$movement = new BovineMovement();
|
||||
$movement->setBovine($bovine);
|
||||
|
||||
if (null !== $bovine->getBuildingCase()) {
|
||||
$movement->setBuildingCase($bovine->getBuildingCase());
|
||||
} else {
|
||||
$movement->setBuilding($bovine->getBuilding());
|
||||
}
|
||||
|
||||
$enteredAt = $bovine->getArrivalDate();
|
||||
if (null === $enteredAt) {
|
||||
$enteredAt = $now;
|
||||
++$fallback;
|
||||
}
|
||||
$movement->setEnteredAt($enteredAt);
|
||||
|
||||
$this->entityManager->persist($movement);
|
||||
++$created;
|
||||
|
||||
if (0 === ($i + 1) % self::FLUSH_EVERY) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success(sprintf('%d mouvement(s) créé(s).', $created));
|
||||
if ($fallback > 0) {
|
||||
$io->warning(sprintf("%d bovin(s) sans date d'arrivée → enteredAt = maintenant.", $fallback));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineRepository;
|
||||
use App\State\Bovin\BovineProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
@@ -135,6 +137,37 @@ class Bovine
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
private ?DateTimeImmutable $exitedAt = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?string $motherNationalNumber = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BovineType $motherBovineType = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?string $fatherNationalNumber = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BovineType $fatherBovineType = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, BovineMovement>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: BovineMovement::class, mappedBy: 'bovine', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['enteredAt' => 'DESC'])]
|
||||
#[Groups(['bovine:read'])]
|
||||
private Collection $movements;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->movements = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -329,6 +362,79 @@ class Bovine
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMotherNationalNumber(): ?string
|
||||
{
|
||||
return $this->motherNationalNumber;
|
||||
}
|
||||
|
||||
public function setMotherNationalNumber(?string $motherNationalNumber): static
|
||||
{
|
||||
$this->motherNationalNumber = $motherNationalNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMotherBovineType(): ?BovineType
|
||||
{
|
||||
return $this->motherBovineType;
|
||||
}
|
||||
|
||||
public function setMotherBovineType(?BovineType $motherBovineType): static
|
||||
{
|
||||
$this->motherBovineType = $motherBovineType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFatherNationalNumber(): ?string
|
||||
{
|
||||
return $this->fatherNationalNumber;
|
||||
}
|
||||
|
||||
public function setFatherNationalNumber(?string $fatherNationalNumber): static
|
||||
{
|
||||
$this->fatherNationalNumber = $fatherNationalNumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFatherBovineType(): ?BovineType
|
||||
{
|
||||
return $this->fatherBovineType;
|
||||
}
|
||||
|
||||
public function setFatherBovineType(?BovineType $fatherBovineType): static
|
||||
{
|
||||
$this->fatherBovineType = $fatherBovineType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, BovineMovement>
|
||||
*/
|
||||
public function getMovements(): Collection
|
||||
{
|
||||
return $this->movements;
|
||||
}
|
||||
|
||||
public function addMovement(BovineMovement $movement): static
|
||||
{
|
||||
if (!$this->movements->contains($movement)) {
|
||||
$this->movements->add($movement);
|
||||
$movement->setBovine($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeMovement(BovineMovement $movement): static
|
||||
{
|
||||
$this->movements->removeElement($movement);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
#[ORM\PreUpdate]
|
||||
public function refreshAgeMonths(): void
|
||||
|
||||
129
src/Entity/BovineMovement.php
Normal file
129
src/Entity/BovineMovement.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Repository\BovineMovementRepository;
|
||||
use App\State\Bovin\BovineMovementProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BovineMovementRepository::class)]
|
||||
#[ORM\Table(name: 'bovine_movement')]
|
||||
#[ORM\Index(name: 'idx_bovine_movement_timeline', columns: ['bovine_id', 'entered_at'])]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Post(
|
||||
denormalizationContext: ['groups' => ['bovine_movement:write']],
|
||||
normalizationContext: ['groups' => ['bovine:read']],
|
||||
processor: BovineMovementProcessor::class,
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_BUREAU')",
|
||||
)]
|
||||
class BovineMovement
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'movements')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['bovine_movement:write'])]
|
||||
private Bovine $bovine;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?BuildingCase $buildingCase = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?Building $building = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
#[Groups(['bovine:read', 'bovine_movement:write'])]
|
||||
private DateTimeImmutable $enteredAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
#[Groups(['bovine:read'])]
|
||||
private ?DateTimeImmutable $leftAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getBovine(): Bovine
|
||||
{
|
||||
return $this->bovine;
|
||||
}
|
||||
|
||||
public function setBovine(Bovine $bovine): static
|
||||
{
|
||||
$this->bovine = $bovine;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuildingCase(): ?BuildingCase
|
||||
{
|
||||
return $this->buildingCase;
|
||||
}
|
||||
|
||||
public function setBuildingCase(?BuildingCase $buildingCase): static
|
||||
{
|
||||
$this->buildingCase = $buildingCase;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBuilding(): ?Building
|
||||
{
|
||||
return $this->building;
|
||||
}
|
||||
|
||||
public function setBuilding(?Building $building): static
|
||||
{
|
||||
$this->building = $building;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnteredAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->enteredAt;
|
||||
}
|
||||
|
||||
public function hasEnteredAt(): bool
|
||||
{
|
||||
return isset($this->enteredAt);
|
||||
}
|
||||
|
||||
public function setEnteredAt(DateTimeImmutable $enteredAt): static
|
||||
{
|
||||
$this->enteredAt = $enteredAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLeftAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->leftAt;
|
||||
}
|
||||
|
||||
public function setLeftAt(?DateTimeImmutable $leftAt): static
|
||||
{
|
||||
$this->leftAt = $leftAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
34
src/Repository/BovineMovementRepository.php
Normal file
34
src/Repository/BovineMovementRepository.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bovine;
|
||||
use App\Entity\BovineMovement;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<BovineMovement>
|
||||
*/
|
||||
final class BovineMovementRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, BovineMovement::class);
|
||||
}
|
||||
|
||||
public function findOpenMovement(Bovine $bovine): ?BovineMovement
|
||||
{
|
||||
return $this->createQueryBuilder('m')
|
||||
->where('m.bovine = :bovine')
|
||||
->andWhere('m.leftAt IS NULL')
|
||||
->setParameter('bovine', $bovine)
|
||||
->orderBy('m.enteredAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
}
|
||||
44
src/State/Bovin/BovineMovementProcessor.php
Normal file
44
src/State/Bovin/BovineMovementProcessor.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State\Bovin;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\BovineMovement;
|
||||
use App\Repository\BovineMovementRepository;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
final class BovineMovementProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BovineMovementRepository $movementRepository,
|
||||
#[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 BovineMovement) {
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$enteredAt = $data->hasEnteredAt() ? $data->getEnteredAt() : new DateTimeImmutable();
|
||||
$data->setEnteredAt($enteredAt);
|
||||
$data->setLeftAt(null);
|
||||
$data->setBuilding(null);
|
||||
|
||||
$bovine = $data->getBovine();
|
||||
|
||||
$openMovement = $this->movementRepository->findOpenMovement($bovine);
|
||||
if (null !== $openMovement) {
|
||||
$openMovement->setLeftAt($enteredAt);
|
||||
}
|
||||
|
||||
$bovine->setBuildingCase($data->getBuildingCase());
|
||||
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,11 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
$bovine->setBovineType($this->resolveBovineType($identification->breedType));
|
||||
$bovine->setWorkNumber($identification->workNumber);
|
||||
$bovine->setBirthDate($identification->birthDate?->date);
|
||||
|
||||
$bovine->setMotherNationalNumber($identification->motherCarrier?->bovin?->nationalNumber);
|
||||
$bovine->setMotherBovineType($this->resolveBovineType($identification->motherCarrier?->breedType));
|
||||
$bovine->setFatherNationalNumber($identification->fatherIpg?->bovin?->nationalNumber);
|
||||
$bovine->setFatherBovineType($this->resolveBovineType($identification->fatherIpg?->breedType));
|
||||
}
|
||||
|
||||
$latestEntry = null;
|
||||
|
||||
@@ -91,6 +91,22 @@ final readonly class BuildingCaseWeightsReportProvider implements ProviderInterf
|
||||
];
|
||||
}
|
||||
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
$aw = (string) ($a['workNumber'] ?? '');
|
||||
$bw = (string) ($b['workNumber'] ?? '');
|
||||
if ('' === $aw && '' === $bw) {
|
||||
return 0;
|
||||
}
|
||||
if ('' === $aw) {
|
||||
return 1;
|
||||
}
|
||||
if ('' === $bw) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int) $aw <=> (int) $bw;
|
||||
});
|
||||
|
||||
$monthHeaders = $this->buildMonthHeaders($firstArrivalDate, $headerBreedCode);
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
.sheet { width: auto; }
|
||||
|
||||
h1 {
|
||||
margin: 8px 0 16px 0;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
@@ -139,10 +139,10 @@
|
||||
}
|
||||
|
||||
.main .sub-title {
|
||||
font-size: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
padding: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.main .base {
|
||||
@@ -203,61 +203,61 @@
|
||||
<h1 style="color: red; text-align: center; width: 100%; font-size: 36px">
|
||||
Arrivage du {{ firstArrivalDate ?? '-' }}
|
||||
</h1>
|
||||
<table style="width:100%; border-collapse:collapse; table-layout:fixed; margin-bottom: 16px">
|
||||
<table style="width:100%; border-collapse:collapse; table-layout:fixed; margin-bottom: 4px">
|
||||
<colgroup>
|
||||
{# 28 colonnes ≈ 3.571% chacune #}
|
||||
{% for _ in 0..27 %}<col style="width:3.571%">{% endfor %}
|
||||
</colgroup>
|
||||
<tr>
|
||||
<td style="width:40%; vertical-align:top; padding-right:2mm; border:0;">
|
||||
<table style="width:100%; border-collapse:collapse; table-layout:fixed;">
|
||||
<tr>
|
||||
<td style="border: 0; height: 20px"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:700; text-align: left; border: none; font-size: 24px">CASE N° {{ buildingCase.caseNumber ?? '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td style="border:0; text-align:left; font-weight:700; font-size: 18px;" colspan="4">PROVENANCE</td>
|
||||
|
||||
<td style="width:60%; vertical-align:top; padding-left:2mm; border:0;">
|
||||
<table class="header-right-free" style="width:100%; border-collapse:collapse; table-layout:fixed;">
|
||||
<tr>
|
||||
<td style="border:0; text-align:center; font-weight:700; height: 20px;" colspan="5"></td>
|
||||
<td style="border:0;" colspan="2"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; height: 20px;">1</td>
|
||||
<td style="border:0; height: 20px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; height: 20px;">2</td>
|
||||
<td style="border:0; height: 20px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; height: 20px;">3</td>
|
||||
<td style="border:0; height: 20px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; height: 20px;">4</td>
|
||||
<td style="border:0;" colspan="2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:0; text-align:left; font-weight:700; font-size: 24px; width:40%; height: 20px;" colspan="5">PROVENANCE</td>
|
||||
<td style="border:0;" colspan="2"></td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border: 0; width: 20%;" colspan="2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 0; height: 20px" colspan="16"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border: 0; text-align:left; font-weight:700; font-size: 24px" colspan="3">RACE</td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="3">LIMOUSIN</td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="1"></td>
|
||||
<td style="border: 0; text-align:center; font-weight:700;" colspan="1"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="3">CHAROLAIS</td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="1"></td>
|
||||
<td style="border: 0; text-align:center; font-weight:700;" colspan="1"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="2">Autre</td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="1"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
{# Paire 1 : chiffre + case vide #}
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; font-size: 11px; padding:0;">1</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
{# Paire 2 #}
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; font-size: 11px; padding:0;">2</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
{# Paire 3 #}
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; font-size: 11px; padding:0;">3</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
{# Paire 4 #}
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700; font-size: 11px; padding:0;">4</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
|
||||
{# Espacement entre PROVENANCE et RACE (1 col, RACE commence plus tôt) #}
|
||||
<td style="border:0;"></td>
|
||||
|
||||
{# Bloc RACE #}
|
||||
<td style="border:0; text-align:left; font-weight:700; font-size: 18px;" colspan="2">RACE</td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="2">LIMOUSIN</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;" colspan="2">CHAROLAIS</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
<td style="border:0;"></td>
|
||||
<td style="border:1px solid #2b2b2b; text-align:center; font-weight:700;">AUTRE</td>
|
||||
<td style="border:1px solid #2b2b2b;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="width:auto; border-collapse:collapse; margin-bottom: 8px; margin-top: 8px">
|
||||
<tr>
|
||||
<td style="border:0; text-align:left; font-weight:700; font-size: 18px; padding-right: 8px;">BATIMENT N°</td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
<td style="border:0; width: 22px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
<td style="border:0; width: 22px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
<td style="border:0; width: 32px;"></td>
|
||||
<td style="border:0; text-align:left; font-weight:700; font-size: 18px; padding-right: 8px;">CASE N°</td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
<td style="border:0; width: 22px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
<td style="border:0; width: 22px;"></td>
|
||||
<td style="border:1px solid #2b2b2b; width: 22px; height: 22px;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -267,30 +267,29 @@
|
||||
<table class="main">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="4" class="head-big" style="width:5%">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" 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([])|reverse %}
|
||||
<th class="month" style="width:6.58%">{{ month.name }}</th>
|
||||
{% endfor %}
|
||||
<th rowspan="4" class="head-big" style="width:7%">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:5%">N° de<br>travail</th>
|
||||
<th rowspan="4" class="head-big" style="width:5%">N° de<br>travail</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
{% for month in monthHeaders|default([]) %}
|
||||
{% for month in monthHeaders|default([])|reverse %}
|
||||
<th class="days">{{ month.days }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="days">Foin</th>
|
||||
<th class="days">Foin</th>
|
||||
<th colspan="{{ monthHeaders|length -2 }}" class="sub-title">POIDS PAR MOIS</th>
|
||||
<th class="days">Foin</th>
|
||||
<th class="days">Foin</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
{% for month in monthHeaders|default([]) %}
|
||||
{% for month in monthHeaders|default([])|reverse %}
|
||||
<th class="base">
|
||||
{% if month.baseValue is defined %}
|
||||
{{ month.baseValue|round(0, 'common') }} kg
|
||||
@@ -303,27 +302,28 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{# 11 lignes comme dans ton code (0..10) #}
|
||||
{# 13 lignes comme dans ton code (0..12) #}
|
||||
{% for i in 0..12 %}
|
||||
{% set row = rows[i] ?? null %}
|
||||
{% set baseWeight = row ? (row.receivedWeight ?? null) : null %}
|
||||
|
||||
<tr class="data-row">
|
||||
<td class="row-work"></td>
|
||||
<td class="row-work">{{ row ? (row.workNumber ?? '') : '' }}</td>
|
||||
<td class="row-weight">{{ baseWeight ?? '' }}</td>
|
||||
{% for idx in 0..(monthCount > 0 ? monthCount - 1 : 0) %}
|
||||
{% set reversedIdx = (monthCount - 1) - idx %}
|
||||
{% set projectedWeight = row and row.projectedWeights is defined ? (row.projectedWeights[reversedIdx] ?? null) : null %}
|
||||
<td class="row-month"{% if reversedIdx < 4 %} style="background:#e0e0e0;"{% endif %}>
|
||||
{{ projectedWeight is not null ? projectedWeight|round(0, 'common') : '' }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="row-birth">
|
||||
{% if row and row.birthDate %}
|
||||
{% set birthParts = row.birthDate|split('/') %}
|
||||
{{ birthParts|length == 3 ? birthParts[1] ~ '/' ~ birthParts[2] : row.birthDate }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for idx in 0..(monthCount > 0 ? monthCount - 1 : 0) %}
|
||||
{% set projectedWeight = row and row.projectedWeights is defined ? (row.projectedWeights[idx] ?? null) : null %}
|
||||
<td class="row-month"{% if loop.index0 < 4 %} style="background:#e0e0e0;"{% endif %}>
|
||||
{{ projectedWeight is not null ? projectedWeight|round(0, 'common') : '' }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="row-weight">{{ baseWeight ?? '' }}</td>
|
||||
<td class="row-work">{{ row ? (row.workNumber ?? '') : '' }}</td>
|
||||
<td class="row-work"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -331,41 +331,89 @@
|
||||
<!-- =========================
|
||||
FOOTER (traitements / vaccins)
|
||||
========================= -->
|
||||
<table class="footer" style="border-collapse:collapse; margin-top: 32px">
|
||||
<table style="width:100%; border:0; border-collapse:collapse; table-layout:fixed; margin-top: 12px">
|
||||
<tr>
|
||||
<td style="height: 20px; border: 0" colspan="4"></td>
|
||||
<td style="font-weight: 700" colspan="2">Grippe</td>
|
||||
<td style="font-weight: 700" colspan="2">Protivity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px">Date</td>
|
||||
<td>Antibiotique</td>
|
||||
<td>Date</td>
|
||||
<td>Antero</td>
|
||||
<td>Date</td>
|
||||
<td>Intranasale</td>
|
||||
<td>Date</td>
|
||||
<td>Rappel 30 jours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px"></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px"></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="border:0; padding:0; width:49%; vertical-align:top;">
|
||||
<table class="footer" style="border-collapse:collapse; width:100%; table-layout:fixed;">
|
||||
<tr>
|
||||
<td style="font-weight: 700; height: 20px" colspan="10">Traitements</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2">Date</td>
|
||||
<td colspan="2"></td>
|
||||
<td>Dose</td>
|
||||
<td colspan="5">Observation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td colspan="2">Grippe</td>
|
||||
<td></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td colspan="2">Antéro</td>
|
||||
<td></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td colspan="2">Antibiotiques</td>
|
||||
<td></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td colspan="2">Déparasitage</td>
|
||||
<td></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td style="border:0; padding:0; width:2%;"></td>
|
||||
<td style="border:0; padding:0; width:49%; vertical-align:top;">
|
||||
<table class="footer" style="border-collapse:collapse; width:100%; table-layout:fixed;">
|
||||
<tr>
|
||||
<td style="font-weight: 700; height: 20px" colspan="10">Rappel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2">Date</td>
|
||||
<td>Dose</td>
|
||||
<td colspan="7">Observation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="height: 20px" colspan="2"></td>
|
||||
<td></td>
|
||||
<td colspan="7"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user