[#FER-26] Passeport du bovin (!53)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #53 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #53.
This commit is contained in:
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>
|
||||
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,182 +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">
|
||||
useHead({ title: 'Bovins' })
|
||||
|
||||
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>
|
||||
@@ -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"
|
||||
>
|
||||
@@ -134,7 +126,6 @@ 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'
|
||||
@@ -143,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)
|
||||
@@ -233,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)
|
||||
@@ -270,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
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user