Compare commits

..

5 Commits

38 changed files with 457 additions and 713 deletions

View File

@@ -16,50 +16,30 @@ jobs:
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}
persist-credentials: true persist-credentials: true
- name: Create next tag from config/version.yaml - name: Create next tag v0.0.X
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
# Skip if current commit already has a vX.Y.Z tag # Skip if current commit already has a v0.0.* tag
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then if git tag --points-at HEAD | grep -qE '^v0\.0\.'; then
echo "Tag already exists on this commit. Skipping." echo "Tag already exists on this commit. Skipping."
exit 0 exit 0
fi fi
changed_version=false last_tag="$(git tag -l 'v0.0.*' --sort=-v:refname | head -n1 || true)"
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then if [ -z "$last_tag" ]; then
changed_version=true next_tag="v0.0.1"
fi else
patch="${last_tag##v0.0.}"
read_version() { if ! [[ "$patch" =~ ^[0-9]+$ ]]; then
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\"" echo "Unexpected tag format: $last_tag" >&2
}
if $changed_version; then
version="$(read_version)"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version in version.yaml: $version" >&2
exit 1 exit 1
fi fi
else next_tag="v0.0.$((patch + 1))"
last_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
if [ -z "$last_tag" ]; then
version="0.1.0"
else
base="${last_tag#v}"
IFS='.' read -r major minor patch <<< "$base"
version="${major}.${minor}.$((patch + 1))"
fi
printf "parameters:\\n app.version: '%s'\\n" "$version" > config/version.yaml
git config user.name "gitea-actions"
git config user.email "gitea-actions@local"
git add config/version.yaml
git commit -m "chore: bump version to v$version" || true
git push origin develop || true
fi fi
tag="v$version" git config user.name "gitea-actions"
git tag "$tag" git config user.email "gitea-actions@local"
git push origin "$tag" git tag "$next_tag"
git push origin "$next_tag"

2
.idea/dataSources.xml generated
View File

@@ -16,4 +16,4 @@
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
</component> </component>
</project> </project>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_1.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_2.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_3.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_4.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
</component>
</project>

174
.idea/workspace.xml generated
View File

@@ -4,16 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : test auto-tag-develop.yml (auto incrément version)"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
<change afterPath="$PROJECT_DIR$/frontend/composables/useAppVersion.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/packages/security.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/packages/security.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/app.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/app.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-bovine-received.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-bovine-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/login.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/login.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Dto/AppVersion.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/ApiResource/AppVersion.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/State/AppVersionProvider.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/State/AppVersionProvider.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/ReceptionBovine.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/ReceptionBovine.php" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -44,7 +43,7 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="feat/256-reception-etape-3-bovin" /> <entry key="$PROJECT_DIR$" value="fix/makefile" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -232,7 +231,7 @@
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;develop&quot;, &quot;git-widget-placeholder&quot;: &quot;feat/256-reception-etape-3-bovin&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/sroy/Documents/test/Ferme&quot;, &quot;last_opened_file_path&quot;: &quot;/home/sroy/Documents/test/Ferme&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
@@ -302,6 +301,70 @@
<workItem from="1770195959162" duration="18915000" /> <workItem from="1770195959162" duration="18915000" />
<workItem from="1770274844804" duration="3940000" /> <workItem from="1770274844804" duration="3940000" />
</task> </task>
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
<option name="closed" value="true" />
<created>1768237763998</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1768237763998</updated>
</task>
<task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
<option name="closed" value="true" />
<created>1768316052474</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1768316052474</updated>
</task>
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
<option name="closed" value="true" />
<created>1768316835575</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1768316835575</updated>
</task>
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
<option name="closed" value="true" />
<created>1768316965511</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1768316965511</updated>
</task>
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
<option name="closed" value="true" />
<created>1768317786187</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1768317786187</updated>
</task>
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
<option name="closed" value="true" />
<created>1768318875533</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1768318875533</updated>
</task>
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
<option name="closed" value="true" />
<created>1768318921478</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1768318921478</updated>
</task>
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
<option name="closed" value="true" />
<created>1768498751836</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1768498751836</updated>
</task>
<task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur"> <task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
<option name="closed" value="true" /> <option name="closed" value="true" />
<created>1768555180530</created> <created>1768555180530</created>
@@ -630,71 +693,7 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1770217875423</updated> <updated>1770217875423</updated>
</task> </task>
<task id="LOCAL-00050" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)"> <option name="localTasksCounter" value="50" />
<option name="closed" value="true" />
<created>1770283622425</created>
<option name="number" value="00050" />
<option name="presentableId" value="LOCAL-00050" />
<option name="project" value="LOCAL" />
<updated>1770283622425</updated>
</task>
<task id="LOCAL-00051" summary="feat : ajout du responsive sur la navbar et la page d'accueil">
<option name="closed" value="true" />
<created>1770308927948</created>
<option name="number" value="00051" />
<option name="presentableId" value="LOCAL-00051" />
<option name="project" value="LOCAL" />
<updated>1770308927948</updated>
</task>
<task id="LOCAL-00052" summary="fix : logo centré en mod mobile">
<option name="closed" value="true" />
<created>1770310504254</created>
<option name="number" value="00052" />
<option name="presentableId" value="LOCAL-00052" />
<option name="project" value="LOCAL" />
<updated>1770310504254</updated>
</task>
<task id="LOCAL-00053" summary="feat : ajout d'un numéro de version automatique via la CI">
<option name="closed" value="true" />
<created>1770369945257</created>
<option name="number" value="00053" />
<option name="presentableId" value="LOCAL-00053" />
<option name="project" value="LOCAL" />
<updated>1770369945257</updated>
</task>
<task id="LOCAL-00054" summary="feat : update numéro de version">
<option name="closed" value="true" />
<created>1770370216428</created>
<option name="number" value="00054" />
<option name="presentableId" value="LOCAL-00054" />
<option name="project" value="LOCAL" />
<updated>1770370216428</updated>
</task>
<task id="LOCAL-00055" summary="fix : auto-tag-develop.yml">
<option name="closed" value="true" />
<created>1770370700697</created>
<option name="number" value="00055" />
<option name="presentableId" value="LOCAL-00055" />
<option name="project" value="LOCAL" />
<updated>1770370700698</updated>
</task>
<task id="LOCAL-00056" summary="fix : auto-tag-develop.yml">
<option name="closed" value="true" />
<created>1770370919043</created>
<option name="number" value="00056" />
<option name="presentableId" value="LOCAL-00056" />
<option name="project" value="LOCAL" />
<updated>1770370919043</updated>
</task>
<task id="LOCAL-00057" summary="feat : test auto-tag-develop.yml (auto incrément version)">
<option name="closed" value="true" />
<created>1770371073055</created>
<option name="number" value="00057" />
<option name="presentableId" value="LOCAL-00057" />
<option name="project" value="LOCAL" />
<updated>1770371073055</updated>
</task>
<option name="localTasksCounter" value="58" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -744,6 +743,12 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="fix : correction du path URI pour la création d'un poids dans une réception" />
<MESSAGE value="feat : Ajout du bundle Monolog pour la gestion des logs" />
<MESSAGE value="fix : affiche plus détail dans les logs en recette/prod" />
<MESSAGE value="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod" />
<MESSAGE value="fix : doc de déploiement" />
<MESSAGE value="fix : doc et script de déploiement" />
<MESSAGE value="fix : gitea workflow" /> <MESSAGE value="fix : gitea workflow" />
<MESSAGE value="fix : script de déploiement" /> <MESSAGE value="fix : script de déploiement" />
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" /> <MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
@@ -763,13 +768,18 @@
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" /> <MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
<MESSAGE value="feat : mise à jour du bon de réception" /> <MESSAGE value="feat : mise à jour du bon de réception" />
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" /> <MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
<MESSAGE value="feat : ajout du responsive sur la navbar et la page d'accueil" /> <option name="LAST_COMMIT_MESSAGE" value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
<MESSAGE value="fix : logo centré en mod mobile" /> </component>
<MESSAGE value="feat : ajout d'un numéro de version automatique via la CI" /> <component name="XDebuggerManager">
<MESSAGE value="feat : update numéro de version" /> <breakpoint-manager>
<MESSAGE value="fix : auto-tag-develop.yml" /> <breakpoints>
<MESSAGE value="feat : test auto-tag-develop.yml (auto incrément version)" /> <line-breakpoint enabled="true" type="php">
<option name="LAST_COMMIT_MESSAGE" value="feat : test auto-tag-develop.yml (auto incrément version)" /> <url>file://$PROJECT_DIR$/src/Entity/ReceptionPelletBuilding.php</url>
<line>6</line>
<option name="timeStamp" value="3" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@@ -27,8 +27,6 @@ Ajouter dans le fichier .env du frontend
* Ajout du bundle malio/ednotif-bundle * Ajout du bundle malio/ednotif-bundle
* Ajout de composant UI * Ajout de composant UI
* Finalisation de la partie réception de marchandise * Finalisation de la partie réception de marchandise
* [#267] Lister les réceptions en attente
* [#268] Lister les réceptions terminées
### Changed ### Changed

View File

@@ -53,8 +53,6 @@ security:
- { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] } - { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] }
# Doc API (swagger) en public # Doc API (swagger) en public
- { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api/docs, roles: PUBLIC_ACCESS }
# Version de l'application en public
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [GET] }
# Tout le reste nécessite un JWT # Tout le reste nécessite un JWT
- { path: ^/, roles: IS_AUTHENTICATED_FULLY } - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

View File

@@ -8,9 +8,6 @@
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
imports:
- { resource: version.yaml }
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
_defaults: _defaults:

View File

@@ -1,2 +0,0 @@
parameters:
app.version: '0.0.32'

View File

@@ -3,11 +3,3 @@
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>
<script setup lang="ts">
const { load } = useAppVersion()
onMounted(() => {
load()
})
</script>

View File

@@ -1,30 +0,0 @@
<template>
<NuxtLink :to="link">
<div class="w-[324px] h-[228px] border border-black rounded-md p-6 flex flex-col justify-between">
<div class="flex justify-between">
<div class="rounded-full w-[80px] h-[80px] bg-neutral-400 flex justify-center items-center">
<Icon :name="iconName" style="color: black" size="44" />
</div>
<div>
<Icon name="mdi:plus" style="color: black" size="44" />
</div>
</div>
<div class="uppercase font-bold">
<p class="text-3xl"> {{ label }} </p>
</div>
</div>
</NuxtLink>
</template>
<script setup lang="ts">
const props = defineProps<{
link: string
iconName: string
label: string
}>()
</script>

View File

@@ -36,7 +36,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type {BovineTypeData} from "~/services/dto/bovine-type-data"; import type {BovineTypeData} from "~/services/dto/bovine-type-data";
import {getBovineTypeList} from "~/services/bovine-type"; import {getBovineTypeList} from "~/services/bovine-type";
import {RECEPTION_TYPE_CODES} from "~/utils/constants"; import {MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES} from "~/utils/constants";
import {useReceptionStore} from '~/stores/reception' import {useReceptionStore} from '~/stores/reception'
import { import {
createReceptionBovine, createReceptionBovine,
@@ -46,7 +46,6 @@ import {
} from "~/services/reception-bovine"; } from "~/services/reception-bovine";
import {computed, onMounted, reactive, ref, watch} from "vue"; import {computed, onMounted, reactive, ref, watch} from "vue";
const toast = useToast()
const isLoadingBovineType = ref(false) const isLoadingBovineType = ref(false)
const bovineType = ref<BovineTypeData[]>([]) const bovineType = ref<BovineTypeData[]>([])
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
@@ -56,13 +55,19 @@ const receptionId = computed(() => receptionStore.current?.id ?? null)
const receptionIri = computed(() => const receptionIri = computed(() =>
receptionId.value ? `/api/receptions/${receptionId.value}` : null receptionId.value ? `/api/receptions/${receptionId.value}` : null
) )
const totalBovines = computed(() => { const toast = useToast()
const base = Object.values(bovineQuantities).reduce((sum, value) => { const nuxtApp = useNuxtApp()
return sum + (value ?? 0) const i18n = nuxtApp.$i18n as { t: (key: string) => string } |
undefined
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
const totalBovineQuantity = computed(() => {
const baseTotal = Object.values(bovineQuantities).reduce((sum, value) => {
const n = typeof value === 'number' ? value : 0
return sum + n
}, 0) }, 0)
return base + (otherQuantity.value ?? 0) const other = typeof otherQuantity.value === 'number' ? otherQuantity.value : 0
return baseTotal + other
}) })
const loadBovineType = async () => { const loadBovineType = async () => {
isLoadingBovineType.value = true isLoadingBovineType.value = true
try { try {
@@ -112,7 +117,6 @@ watch(
async function syncBovineSelections(receptionIri: string) { async function syncBovineSelections(receptionIri: string) {
const existing = await getReceptionBovineList(receptionIri) const existing = await getReceptionBovineList(receptionIri)
const existingMap = new Map<string, { id: number; quantity: number | null }>() const existingMap = new Map<string, { id: number; quantity: number | null }>()
for (const selection of existing) { for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id) const bovineTypeId = String(selection.bovineType.id)
existingMap.set(bovineTypeId, { existingMap.set(bovineTypeId, {
@@ -155,21 +159,37 @@ async function syncBovineSelections(receptionIri: string) {
}) })
} }
} }
const hasNegativeQuantity = computed(() => {
const anyNegativeInTypes =
Object.values(bovineQuantities).some((value) => {
return typeof value === 'number' && value < 0
})
const otherNegative =
typeof otherQuantity.value === 'number' &&
otherQuantity.value < 0
return anyNegativeInTypes || otherNegative
})
async function goNext() { async function goNext() {
if (!receptionStore.current || !receptionIri.value) { if (!receptionStore.current || !receptionIri.value) {
return return
} }
if (hasNegativeQuantity.value) {
// @TODO Ajouter un composable pour le toaster qui gère les key i18n toast.error({
if (totalBovines.value > 52) { title: 'Erreur',
message: ("La quantité de bovins ne peut pas être négative.")
})
return
}
// Le 52 à vérifier
if (totalBovineQuantity.value > 52) {
toast.error({ toast.error({
title: 'Erreur', title: 'Erreur',
message: ('Le total des bovins ne peut pas dépasser 52.') message: ('Le total des bovins ne peut pas dépasser 52.')
}) })
return return
} }
const nextStep = receptionStore.current.currentStep + 1 const nextStep = receptionStore.current.currentStep + 1
await syncBovineSelections(receptionIri.value) await syncBovineSelections(receptionIri.value)

View File

@@ -222,12 +222,10 @@ const filteredVehicles = computed<VehicleData[]>(() => {
(!form.truckId || String(vehicle.truck?.id) === form.truckId) (!form.truckId || String(vehicle.truck?.id) === form.truckId)
) )
}) })
const selectedReceptionType = computed(() => const selectedReceptionType = computed(() =>
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
) )
// Supprime les données bovines si on change de type de réception
const clearReceptionBovines = async (receptionIri: string) => { const clearReceptionBovines = async (receptionIri: string) => {
const existing = await getReceptionBovineList(receptionIri) const existing = await getReceptionBovineList(receptionIri)
for (const selection of existing) { for (const selection of existing) {
@@ -473,6 +471,9 @@ async function validate() {
const normalizedTruckId = form.truckId.trim() const normalizedTruckId = form.truckId.trim()
const normalizedCarrierId = form.carrierId.trim() const normalizedCarrierId = form.carrierId.trim()
const normalizedDriverId = form.driverId.trim() const normalizedDriverId = form.driverId.trim()
const previousTypeCode = receptionStore.current.receptionType?.code ?? null
const nextTypeCode = selectedReceptionType.value?.code ?? null
const receptionIri = `/api/receptions/${receptionStore.current.id}`
const receptionTypeIri = normalizedReceptionTypeId const receptionTypeIri = normalizedReceptionTypeId
? `/api/reception_types/${normalizedReceptionTypeId}` ? `/api/reception_types/${normalizedReceptionTypeId}`
: null : null
@@ -521,11 +522,6 @@ async function validate() {
} }
return return
} }
const previousTypeCode = receptionStore.current.receptionType?.code ?? null
const nextTypeCode = selectedReceptionType.value?.code ?? null
const receptionIri = `/api/receptions/${receptionStore.current.id}`
if ( if (
previousTypeCode === RECEPTION_TYPE_CODES.BOVINS && previousTypeCode === RECEPTION_TYPE_CODES.BOVINS &&
nextTypeCode === RECEPTION_TYPE_CODES.MERCHANDISES nextTypeCode === RECEPTION_TYPE_CODES.MERCHANDISES

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="flex flex-col items-center gap-16"> <div class="flex flex-col items-center gap-16">
<!-- @TODO voir pour séparer dans un composant au moment de l'implémentation des Bovins -->
<div <div
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES" v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
class="flex flex-col gap-16 items-center w-full"> class="flex flex-col gap-16 items-center w-full">
@@ -98,6 +99,7 @@ const selectedBuildingIds = ref<string[]>([])
const selectedPelletBuildingIds = ref<Record<string, string[]>>({}) const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
const merchandiseDetail = ref('') const merchandiseDetail = ref('')
// Extrait l'ID d'une relation depuis un IRI ou un objet complet. // Extrait l'ID d'une relation depuis un IRI ou un objet complet.
const getRelationId = (value: unknown): string | null => { const getRelationId = (value: unknown): string | null => {
if (!value) { if (!value) {

View File

@@ -30,7 +30,6 @@
disabled ? 'cursor-not-allowed' : 'cursor-text', disabled ? 'cursor-not-allowed' : 'cursor-text',
inputClass inputClass
]" ]"
@keydown="onKeydown"
@input="onInput" @input="onInput"
> >
</div> </div>
@@ -79,13 +78,7 @@ const onInput = (event: Event) => {
emit('update:modelValue', null) emit('update:modelValue', null)
return return
} }
const numeric = Math.max(0, Number(target.value)) const numeric = Number(target.value)
emit('update:modelValue', Number.isNaN(numeric) ? null : numeric) emit('update:modelValue', Number.isNaN(numeric) ? null : numeric)
} }
const onKeydown = (event: KeyboardEvent) => {
if (event.key === '-' || event.key === 'e' || event.key === 'E') {
event.preventDefault()
}
}
</script> </script>

View File

@@ -1,17 +0,0 @@
export const useAppVersion = () => {
const api = useApi()
const version = useState<string | null>('app-version', () => null)
const load = async () => {
if (version.value) {
return version.value
}
const response = await api.get<{ version: string }>('version', {}, {
toast: false
})
version.value = response.version
return version.value
}
return { version, load }
}

View File

@@ -1,71 +0,0 @@
<template>
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
<!-- HEADER -->
<header class="bg-primary-500 z-50 h-[85px]">
<div class="h-full w-full px-6 grid grid-cols-[auto,1fr,auto] items-center gap-8">
<NuxtLink to="/" class="grid place-items-center">
<span class="grid place-items-center bg-white text-xl font-bold uppercase text-primary-500 p-4">
LOGO
</span>
</NuxtLink>
<nav class="text-2xl font-bold uppercase text-white"></nav>
<NuxtLink
to="/"
class="text-xl font-bold uppercase text-white transition hover:opacity-80 justify-self-end"
>
Quitter le panel admin
</NuxtLink>
</div>
</header>
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
<aside class="bg-primary-500 text-white min-h-0 grid grid-rows-[auto,1fr,auto]">
<div class="p-4 font-semibold">Tableau de bord</div>
<div class="overflow-y-auto min-h-0 p-4 space-y-3">
<!-- Liste des liens à ajouter ci-dessous -->
<!--Button pour afficher le component admin-users -->
<NuxtLink
to="/admin/user-list"
class="block px-4 py-2 rounded hover:bg-primary-600 transition"
>
Utilisateurs
</NuxtLink>
</div>
<div class="p-4">
<p class="font-bold text-white text-left">v{{ version }}</p>
<button
@click="handleLogout"
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
>
Déconnexion
</button>
</div>
</aside>
<main class="min-h-0 overflow-auto px-12 py-12 ">
<div class="w-full ">
<slot />
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import auth from "~/layouts/auth.vue";
const { version } = useAppVersion()
const handleLogout = async () => {
try {
await auth.logout()
} finally {
await navigateTo('/login')
}
}
</script>

View File

@@ -1,16 +1,15 @@
<template> <template>
<div class="min-h-screen bg-white text-neutral-900"> <div class="min-h-screen bg-white text-neutral-900">
<header class="w-full border-b border-neutral-200 bg-primary-500"> <header class="w-full border-b border-neutral-200 bg-primary-500">
<div class="flex w-full items-center justify-center px-6 py-4"> <div class="flex w-full items-center px-6 py-4">
<button <NuxtLink to="/" class="flex items-center gap-3">
type="button" <span
class="inline-flex items-center justify-center text-3xl text-white md:hidden" class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
aria-label="Ouvrir le menu" >
@click="toggleMenu" LOGO
> </span>
<span aria-hidden="true" class="flex items-center"><Icon name="mdi:menu" size="44"/></span> </NuxtLink>
</button> <nav class="mx-8 flex flex-1 gap-8 text-2xl font-bold uppercase text-white">
<nav class="ml-4 hidden items-center gap-8 text-2xl font-bold uppercase text-white md:flex">
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }"> <NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
<a <a
:href="href" :href="href"
@@ -20,89 +19,28 @@
Accueil Accueil
</a> </a>
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }"> <NuxtLink to="/reception" custom v-slot="{ href, navigate, isActive }">
<a <a
:href="href" :href="href"
@click="navigate" @click="navigate"
:class="isReceptionActive ? 'opacity-100' : 'opacity-50'"
> >
Admin Reception
</a> </a>
</NuxtLink> </NuxtLink>
</nav> </nav>
<NuxtLink to="/" class="flex flex-1 items-center justify-center gap-3">
<span
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
>
LOGO
</span>
</NuxtLink>
<div class="w-[44px] md:hidden"></div>
<button <button
type="button" type="button"
class="ml-auto hidden text-xl font-bold uppercase text-white transition hover:opacity-80 md:inline-flex" class="ml-auto text-xl font-bold uppercase text-white transition hover:opacity-80"
@click="handleLogout" @click="handleLogout"
> >
Déconnexion Déconnexion
</button> </button>
</div> </div>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="isMenuOpen"
class="fixed inset-0 z-40 bg-black/40 md:hidden"
@click="closeMenu"
/>
</transition>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="-translate-x-full"
enter-to-class="translate-x-0"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-x-0"
leave-to-class="-translate-x-full"
>
<aside
v-if="isMenuOpen"
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-600 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
role="dialog"
aria-modal="true"
>
<div class="flex items-center justify-between">
<span class="text-2xl font-bold uppercase">Menu</span>
<button
type="button"
class="text-2xl"
aria-label="Fermer le menu"
@click="closeMenu"
>
<Icon name="mdi:close" size="44"/>
</button>
</div>
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
<NuxtLink to="/" class="opacity-100" @click="closeMenu">Accueil</NuxtLink>
</nav>
<button
type="button"
class="mt-5 text-xl font-bold uppercase"
@click="handleLogout"
>
Déconnexion
</button>
</aside>
</transition>
</header> </header>
<main class="mx-auto w-full max-w-[1280px] pb-0"> <main class="mx-auto w-full max-w-[1280px] px-6 pt-[85px] pb-0">
<slot/> <slot/>
</main> </main>
<footer class="w-full mt-8 bg-primary-500 p-6">
<p class="font-bold text-white text-right">v{{ version }}</p>
</footer>
</div> </div>
</template> </template>
@@ -111,22 +49,12 @@ import { useAuthStore } from '~/stores/auth'
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const isMenuOpen = ref(false) const isReceptionActive = computed(() => route.path.startsWith('/reception'))
const { version } = useAppVersion()
const closeMenu = () => {
isMenuOpen.value = false
}
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value
}
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await auth.logout() await auth.logout()
} finally { } finally {
closeMenu()
await navigateTo('/login') await navigateTo('/login')
} }
} }

View File

@@ -9,8 +9,7 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss', '@nuxtjs/tailwindcss',
'@pinia/nuxt', '@pinia/nuxt',
'nuxt-toast', 'nuxt-toast',
'@nuxtjs/i18n', '@nuxtjs/i18n'
'@nuxt/icon'
], ],
css: ['~/assets/css/main.css', '~/assets/css/toast.css'], css: ['~/assets/css/main.css', '~/assets/css/toast.css'],
runtimeConfig: { runtimeConfig: {

View File

@@ -7,7 +7,6 @@
"name": "frontend", "name": "frontend",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
@@ -36,19 +35,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@antfu/install-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
"license": "MIT",
"dependencies": {
"package-manager-detector": "^1.3.0",
"tinyexec": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -1262,47 +1248,6 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@iconify/collections": {
"version": "1.0.646",
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.646.tgz",
"integrity": "sha512-zA5Gr1MJm1SI0TjOUl7wu4kvBWXQ6Uh8ALEtqQ5ucXyUxP2M8m2bk2hfVtGykSdMlDB+Xs2AHbJ9pQqayz9WGQ==",
"license": "MIT",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"license": "MIT"
},
"node_modules/@iconify/utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz",
"integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==",
"license": "MIT",
"dependencies": {
"@antfu/install-pkg": "^1.1.0",
"@iconify/types": "^2.0.0",
"mlly": "^1.8.0"
}
},
"node_modules/@iconify/vue": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-5.0.0.tgz",
"integrity": "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==",
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"vue": ">=3"
}
},
"node_modules/@intlify/bundle-utils": { "node_modules/@intlify/bundle-utils": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-11.0.3.tgz", "resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-11.0.3.tgz",
@@ -2323,28 +2268,6 @@
"devtools-wizard": "cli.mjs" "devtools-wizard": "cli.mjs"
} }
}, },
"node_modules/@nuxt/icon": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-2.2.1.tgz",
"integrity": "sha512-GI840yYGuvHI0BGDQ63d6rAxGzG96jQcWrnaWIQKlyQo/7sx9PjXkSHckXUXyX1MCr9zY6U25Td6OatfY6Hklw==",
"license": "MIT",
"dependencies": {
"@iconify/collections": "^1.0.641",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^3.1.0",
"@iconify/vue": "^5.0.0",
"@nuxt/devtools-kit": "^3.1.1",
"@nuxt/kit": "^4.2.2",
"consola": "^3.4.2",
"local-pkg": "^1.1.2",
"mlly": "^1.8.0",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^3.10.0",
"tinyglobby": "^0.2.15"
}
},
"node_modules/@nuxt/kit": { "node_modules/@nuxt/kit": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz", "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz",

View File

@@ -11,7 +11,6 @@
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist" "build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
}, },
"dependencies": { "dependencies": {
"@nuxt/icon": "^2.2.1",
"@nuxtjs/i18n": "^10.2.1", "@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3", "@pinia/nuxt": "^0.11.3",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",

View File

@@ -1,13 +0,0 @@
<template>
<admin-users v-if="activeCode === 'users'" />
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
const route = useRoute()
const activeCode = computed(() => (route.query.code as string))
</script>

View File

@@ -1,64 +0,0 @@
<template>
<div class="flex items-center justify-between gap-10">
<h1 class="text-3xl font-bold uppercase">Utilisateurs</h1>
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
@click="null"
>
Ajouter
</button>
</div>
<div>
<div class="mt-6 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>Username</div>
<div>Role</div>
<div>Action</div>
</div>
<div
v-for="user in userList"
:key="user.id"
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
role="button"
tabindex="0"
>
<div>
{{ user.username}}
</div>
<div>
{{ user.roles?.join(', ') || ' ---' }}
</div>
<div>
<div class="p-4">
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
@click="null"
>
Modifier
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
import type {UserData} from "~/services/dto/user-data";
import {getUsers} from "~/services/auth";
const userList = ref<UserData[]>([])
onMounted(async () => {
userList.value = await getUsers()
})
</script>

View File

@@ -1,15 +1,55 @@
<script setup lang="ts">
</script>
<template> <template>
<div class="flex flex-wrap justify-center mt-8 gap-8 mb-8 md:mb-0"> <div>
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" /> <h1 class="text-3xl font-bold">Liste des receptions</h1>
<card-link label="NOUVELLE EXPÉDITION" link="/" iconName="mdi:truck-fast-outline" /> <div class="mt-6 border border-slate-200">
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" /> <div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" /> <div>ID</div>
<card-link label="EXPÉDITIONS EN ATTENTE" link="/" iconName="mdi:truck-cargo-container" /> <div>Immatriculation</div>
<card-link label="CASES" link="/" iconName="mdi:cube-outline" /> <div>Pesée plein</div>
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" /> <div>Pesée vide</div>
<card-link label="EXPÉDITIONS FINIES" link="/" iconName="mdi:truck-delivery-outline" /> <div>Etape</div>
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" /> <div>Date</div>
</div>
<div
v-for="reception in receptionList"
:key="reception.id"
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToReception(reception.id)"
@keydown.enter="goToReception(reception.id)"
>
<div>{{ reception.id }}</div>
<div>{{ reception.licensePlate }}</div>
<div>{{ formatWeighing(reception, 'gross') }}</div>
<div>{{ formatWeighing(reception, 'tare') }}</div>
<div>{{ reception.currentStep }}</div>
<div>{{ reception.receptionDate }}</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception";
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const goToReception = (id: number) => {
router.push(`/reception/${id}`)
}
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => {
const entry = reception.weights?.find((weight) => weight.type === type)
if (!entry || entry.weight == null || entry.dsd == null) {
return '—'
}
return `${entry.weight} kg / ${entry.dsd} dsd`
}
onMounted(async () => {
receptionList.value = await getReceptionList()
})
</script>

View File

@@ -46,8 +46,7 @@
> >
Connexion Connexion
</button> </button>
<p class="font-bold">v{{ version }}</p> </form>
</form>
</div> </div>
</template> </template>
@@ -58,7 +57,6 @@ import { useAuthStore } from '~/stores/auth'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const { version } = useAppVersion()
definePageMeta({ definePageMeta({
layout: 'auth' layout: 'auth'

View File

@@ -1,53 +0,0 @@
<template>
<div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
<h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1>
</div>
<div class="ps-20 " >
<div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Numéro</div>
<div>Date</div>
<div>Fournisseur</div>
<div>Adresse</div>
<div>Type réception</div>
<div>Poids</div>
</div>
<div
v-for="reception in receptionList"
:key="reception.id"
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
>
<div>{{ reception.identificationNumber}}</div>
<div>{{ reception.receptionDate}}</div>
<div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div>
<div>{{ formatWeighing(reception, 'gross') }} | {{ formatWeighing(reception, 'tare') }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception";
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => {
const entry = reception.weights?.find((weight) => weight.type === type)
if (!entry || entry.weight == null || entry.dsd == null) {
return '—'
}
return `${entry.weight} kg`
}
onMounted(async () => {
receptionList.value = await getReceptionList(true)
})
</script>

View File

@@ -1,51 +0,0 @@
<template>
<div class="flex items-center justify-between ">
<div class="flex items-center gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
<h1 class="text-3xl font-bold uppercase">listes des réceptions en attente</h1>
</div>
</div>
<div class="ps-20 " >
<div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Fournisseur</div>
<div>Adresse</div>
<div>Type réception</div>
<div>Transporteur</div>
<div>Immatriculation</div>
</div>
<div
v-for="reception in receptionList"
:key="reception.id"
class="grid grid-cols-5 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToReception(reception.id)"
@keydown.enter="goToReception(reception.id)"
>
<div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div>
<div>{{ reception.carrier?.name }}</div>
<div>{{ reception.licensePlate }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception";
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const goToReception = (id: number) => {
router.push(`/reception/${id}`)
}
onMounted(async () => {
receptionList.value = await getReceptionList(false)
})
</script>

View File

@@ -1,5 +1,4 @@
export interface UserData { export interface UserData {
id: number id: number
username: string username: string
roles?: string[]
} }

View File

@@ -2,15 +2,13 @@ import {useApi} from '~/composables/useApi'
import type {ReceptionData, ReceptionPayload} from '~/services/dto/reception-data' import type {ReceptionData, ReceptionPayload} from '~/services/dto/reception-data'
import type {WeightData} from '~/services/dto/weight-data' import type {WeightData} from '~/services/dto/weight-data'
export async function getReceptionList(isValid: boolean|null = null) { export async function getReceptionList() {
const api = useApi() const api = useApi()
const query = isValid !== null ? { isValid: isValid} : {} return api.get<ReceptionData>(`receptions`, {}, {
return api.get<ReceptionData[]>('receptions', query, {
toastErrorKey: 'errors.reception.list' toastErrorKey: 'errors.reception.list'
}) })
} }
export async function getReception(id: number) { export async function getReception(id: number) {
const api = useApi() const api = useApi()
return api.get<ReceptionData>(`receptions/${id}`, {}, { return api.get<ReceptionData>(`receptions/${id}`, {}, {

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\State\AppVersionProvider;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/version',
normalizationContext: ['groups' => ['version:read']],
provider: AppVersionProvider::class,
),
],
)]
final class AppVersion
{
#[Groups(['version:read'])]
public string $version = '';
}

0
src/Entity/.gitignore vendored Normal file
View File

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
@@ -28,7 +26,6 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity] #[ORM\Entity]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'reception')] #[ORM\Table(name: 'reception')]
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get( new Get(

View File

@@ -49,7 +49,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private string $username = ''; private string $username = '';
#[ORM\Column(type: 'json')] #[ORM\Column(type: 'json')]
#[Groups(['user:read'])]
private array $roles = []; private array $roles = [];
#[ORM\Column] #[ORM\Column]

0
src/Repository/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\BovineType;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<BovineType>
*/
class BovineTypeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BovineType::class);
}
// /**
// * @return BovineType[] Returns an array of BovineType objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('b.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?BovineType
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\ReceptionBovine;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ReceptionBovine>
*/
class ReceptionBovineRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ReceptionBovine::class);
}
// /**
// * @return ReceptionBovine[] Returns an array of ReceptionBovine objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?ReceptionBovine
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\AppVersion;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final readonly class AppVersionProvider implements ProviderInterface
{
public function __construct(
#[Autowire('%app.version%')]
private string $version,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): AppVersion
{
$dto = new AppVersion();
$dto->version = $this->version;
return $dto;
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Tests\State;
use ApiPlatform\Metadata\Operation;
use App\State\BovinIdentificationProvider;
use DateTimeImmutable;
use Malio\EdnotifBundle\Bovin\Api\BovinApiInterface;
use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto;
use Malio\EdnotifBundle\Bovin\Dto\BovinIdentificationDto;
use Malio\EdnotifBundle\Bovin\Dto\BovinRef;
use Malio\EdnotifBundle\Bovin\Dto\DateValueDto;
use Malio\EdnotifBundle\Bovin\Dto\ExploitationRef;
use Malio\EdnotifBundle\Bovin\Dto\MovementDto;
use Malio\EdnotifBundle\Bovin\Dto\ParentInfoDto;
use Malio\EdnotifBundle\Bovin\Dto\PresencePeriodDto;
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
final class BovinIdentificationProviderTest extends TestCase
{
public function testReturnsNullWhenNumeroNationalMissing(): void
{
$api = $this->createMock(BovinApiInterface::class);
$api->expects(self::never())->method('getAnimalFile');
$provider = new BovinIdentificationProvider($api);
$result = $provider->provide($this->createStub(Operation::class), []);
self::assertNull($result);
}
public function testMapsIdentificationAndPresencePeriods(): void
{
$api = $this->createMock(BovinApiInterface::class);
$identification = new BovinIdentificationDto(
bovin: new BovinRef('FR', 'IGNORED'),
sex: 'F',
breedType: 'LIM',
birthDate: new DateValueDto(new DateTimeImmutable('2024-01-02'), 'Y'),
workNumber: 'W123',
isFilie: true,
motherCarrier: new ParentInfoDto(new BovinRef('FR', 'MOM1'), 'CHA'),
fatherIpg: new ParentInfoDto(new BovinRef('FR', 'DAD1'), 'BBB'),
birthExploitation: new ExploitationRef('FR', 'EXP1'),
);
$presencePeriods = [
new PresencePeriodDto(
new MovementDto(new DateTimeImmutable('2024-03-01'), 'ENTRY', null),
new MovementDto(new DateTimeImmutable('2024-03-10'), 'EXIT', null),
),
];
$animalFile = new AnimalFileDto(
new StandardResponseDto(true, null),
$identification,
$presencePeriods,
null
);
$api->expects(self::once())
->method('getAnimalFile')
->with('FR123', 'FR')
->willReturn($animalFile)
;
$provider = new BovinIdentificationProvider($api);
$result = $provider->provide(
$this->createStub(Operation::class),
['numeroNational' => 'FR123']
);
self::assertNotNull($result);
self::assertSame('FR123', $result->numeroNational);
self::assertSame('F', $result->sex);
self::assertSame('LIM', $result->breedType);
self::assertSame('W123', $result->workNumber);
self::assertSame('2024-01-02', $result->birthDate);
self::assertSame('Y', $result->birthDateCompletenessFlag);
self::assertTrue($result->isFilie);
self::assertSame('MOM1', $result->motherNationalNumber);
self::assertSame('CHA', $result->motherBreedType);
self::assertSame('DAD1', $result->fatherNationalNumber);
self::assertSame('BBB', $result->fatherBreedType);
self::assertSame('EXP1', $result->birthExploitationNumber);
self::assertCount(1, $result->presencePeriods);
self::assertSame('2024-03-01', $result->presencePeriods[0]->entryDate);
self::assertSame('ENTRY', $result->presencePeriods[0]->entryCause);
self::assertSame('2024-03-10', $result->presencePeriods[0]->exitDate);
self::assertSame('EXIT', $result->presencePeriods[0]->exitCause);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Tests\State;
use ApiPlatform\Metadata\Operation;
use App\Entity\User;
use App\State\MeProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
/**
* @internal
*/
final class MeProviderTest extends TestCase
{
public function testProvideReturnUser(): void
{
$user = new User();
$security = $this->createStub(Security::class);
$security->method('getUser')->willReturn($user);
$provider = new MeProvider($security);
$result = $provider->provide($this->createStub(Operation::class));
self::assertSame($user, $result);
self::assertInstanceOf(User::class, $result);
}
public function testProvideThrowAccessDeniedException(): void
{
$user = null;
$security = $this->createStub(Security::class);
$security->method('getUser')->willReturn($user);
$provider = new MeProvider($security);
$this->expectException(AccessDeniedException::class);
$this->expectExceptionMessage('User not authenticated.');
$provider->provide($this->createStub(Operation::class));
}
}