feat : améliorations page inventory et filtre date masqué
- Colonnes Bâtiment et Case ajoutées sur inventory (inline buildingCase via readableLink) - Bouton Rafraîchir repositionné dans l'en-tête du tableau (pattern case.vue) - Sync : date du jour pour l'appel EDNOTIF, extraction de la dernière exit date - UiDateMaskedInput : nouveau composant date masqué JJ/MM/AAAA - Propagation du masque date sur tous les datatables (reception, shipment, case, inventory) - Label de colonne "Date et heure" raccourci en "Date" - Champ exitDate ajouté en back (caché côté front, prêt pour future feature) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
frontend/components/ui/UiDateMaskedInput.vue
Normal file
108
frontend/components/ui/UiDateMaskedInput.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div :class="['flex flex-col', wrapperClass]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="font-bold uppercase text-xl text-primary-700"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
:id="id"
|
||||
v-maska="'##/##/####'"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:value="displayValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
class="w-full min-w-0 border-b border-primary-700 bg-transparent"
|
||||
:class="[
|
||||
sizeClass,
|
||||
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||
inputClass
|
||||
]"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vMaska } from 'maska/vue'
|
||||
import { computed, ref, useAttrs, watch } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
label?: string
|
||||
modelValue: string | null | undefined
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
size?: 'default' | 'compact'
|
||||
wrapperClass?: string
|
||||
labelClass?: string
|
||||
inputClass?: string
|
||||
}>(),
|
||||
{
|
||||
placeholder: 'JJ/MM/AAAA',
|
||||
disabled: false,
|
||||
size: 'default',
|
||||
wrapperClass: '',
|
||||
labelClass: '',
|
||||
inputClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const toDisplay = (iso: string | null | undefined): string => {
|
||||
if (!iso) return ''
|
||||
const parts = iso.split('-')
|
||||
if (parts.length !== 3) return ''
|
||||
const [year, month, day] = parts
|
||||
if (year.length !== 4 || month.length !== 2 || day.length !== 2) return ''
|
||||
return `${day}/${month}/${year}`
|
||||
}
|
||||
|
||||
const toIso = (display: string): string | null => {
|
||||
const match = display.match(/^(\d{2})\/(\d{2})\/(\d{4})$/)
|
||||
if (!match) return null
|
||||
const [, day, month, year] = match
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const displayValue = ref(toDisplay(props.modelValue))
|
||||
|
||||
watch(() => props.modelValue, (newIso) => {
|
||||
const expected = toDisplay(newIso)
|
||||
if (expected !== displayValue.value) {
|
||||
displayValue.value = expected
|
||||
}
|
||||
})
|
||||
|
||||
const isEmpty = computed(() => !displayValue.value)
|
||||
const sizeClass = computed(() =>
|
||||
props.size === 'compact'
|
||||
? 'text-sm h-8 font-normal normal-case tracking-normal'
|
||||
: 'text-xl py-[6px]'
|
||||
)
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
displayValue.value = target.value
|
||||
if (target.value === '') {
|
||||
emit('update:modelValue', '')
|
||||
return
|
||||
}
|
||||
const iso = toIso(target.value)
|
||||
emit('update:modelValue', iso ?? '')
|
||||
}
|
||||
</script>
|
||||
@@ -59,7 +59,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header-arrivalDate>
|
||||
<UiDateInput v-model="arrivalDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="arrivalDateFilter" placeholder="Date d'arrivée" size="compact" />
|
||||
</template>
|
||||
<template #cell-arrivalDate="{ item }">
|
||||
{{ formatDate(item.arrivalDate) }}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center 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">Inventaire bovins</h1>
|
||||
</div>
|
||||
<button
|
||||
v-if="auth.isAdmin"
|
||||
type="button"
|
||||
:disabled="syncing"
|
||||
class="inline-flex items-center gap-2 rounded bg-primary-500 px-4 h-[50px] text-white uppercase text-lg hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
@click="syncInventory"
|
||||
>
|
||||
<Icon name="mdi:sync" size="24" :class="syncing ? 'animate-spin' : ''" />
|
||||
{{ syncing ? 'Synchronisation…' : 'Rafraîchir' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-[86px]">
|
||||
<div class="flex items-center justify-between relative">
|
||||
<div class="flex flex-row absolute -left-[60px]">
|
||||
<Icon
|
||||
@click="router.push('/')"
|
||||
name="gg:arrow-left-o"
|
||||
size="44"
|
||||
class="cursor-pointer text-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="font-bold text-3xl uppercase text-primary-500">Inventaire bovins</h1>
|
||||
<button
|
||||
v-if="auth.isAdmin"
|
||||
type="button"
|
||||
:disabled="syncing"
|
||||
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 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
@click="syncInventory"
|
||||
>
|
||||
<Icon name="mdi:sync" size="28" :class="syncing ? 'animate-spin' : ''" />
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 mb-16">
|
||||
<UiDataTable
|
||||
v-model:page="page"
|
||||
@@ -43,13 +48,13 @@
|
||||
<template #header-sex>
|
||||
<UiSelect
|
||||
v-model="filters.sex"
|
||||
placeholder="Sex"
|
||||
placeholder="Sexe"
|
||||
:options="sexOptions"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
<template #header-birthDate>
|
||||
<UiDateInput v-model="birthDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="birthDateFilter" size="compact" placeholder="Né le" />
|
||||
</template>
|
||||
<template #header-breedCode>
|
||||
<UiTextInput
|
||||
@@ -59,7 +64,13 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header-arrivalDate>
|
||||
<UiDateInput v-model="arrivalDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="arrivalDateFilter" size="compact" placeholder="Entrée le" />
|
||||
</template>
|
||||
<template #header-buildingCase.building.label>
|
||||
<UiTextInput :model-value="''" placeholder="Bâtiment" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-buildingCase.caseNumber>
|
||||
<UiTextInput :model-value="''" placeholder="Case" size="compact" disabled />
|
||||
</template>
|
||||
<template #cell-birthDate="{ item }">
|
||||
{{ formatDate(item.birthDate) }}
|
||||
@@ -67,11 +78,18 @@
|
||||
<template #cell-arrivalDate="{ item }">
|
||||
{{ formatDate(item.arrivalDate) }}
|
||||
</template>
|
||||
<template #cell-buildingCase.building.label="{ item }">
|
||||
{{ item.buildingCase?.building?.label ?? '—' }}
|
||||
</template>
|
||||
<template #cell-buildingCase.caseNumber="{ item }">
|
||||
{{ item.buildingCase?.caseNumber ?? '—' }}
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BovineData } from '~/services/dto/bovine-data'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
@@ -160,9 +178,11 @@ const birthDateFilter = singleDateFilter('birthDate[after]', 'birthDate[strictly
|
||||
const columns = [
|
||||
{ key: 'nationalNumber', label: 'N° National', width: '160px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '110px' },
|
||||
{ key: 'sex', label: 'Sex', width: '90px' },
|
||||
{ key: 'sex', label: 'Sexe', width: '90px' },
|
||||
{ key: 'birthDate', label: 'Né le', width: '120px' },
|
||||
{ key: 'breedCode', label: 'Race' },
|
||||
{ key: 'buildingCase.building.label', label: 'Bâtiment' },
|
||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '80px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '120px' }
|
||||
]
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header-receptionDate>
|
||||
<UiDateInput
|
||||
<UiDateMaskedInput
|
||||
v-model="receptionDateFilter"
|
||||
placeholder="Date"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
@@ -119,7 +120,7 @@ const receptionDateFilter = computed<string>({
|
||||
|
||||
const columns = [
|
||||
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
|
||||
{ key: 'receptionDate', label: 'Date et heure', width: '120px' },
|
||||
{ key: 'receptionDate', label: 'Date', width: '120px' },
|
||||
{ key: 'supplier.name', label: 'Fournisseur', width: '1.5fr' },
|
||||
{ key: 'address.fullAddress', label: 'Adresse', width: '2fr' },
|
||||
{ key: 'receptionType.label', label: 'Type réception', width: '0.9fr' },
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
@row-click="goToReception"
|
||||
>
|
||||
<template #header-receptionDate>
|
||||
<UiDateInput v-model="receptionDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="receptionDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-supplier.name>
|
||||
<UiTextInput
|
||||
@@ -122,7 +122,7 @@ const receptionDateFilter = computed<string>({
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ key: 'receptionDate', label: 'Date et heure', width: '120px' },
|
||||
{ key: 'receptionDate', label: 'Date', width: '120px' },
|
||||
{ key: 'supplier.name', label: 'Fournisseur', width: '1.5fr' },
|
||||
{ key: 'address.fullAddress', label: 'Adresse', width: '2fr' },
|
||||
{ key: 'receptionType.label', label: 'Type réception', width: '1.1fr' },
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header-shipmentDate>
|
||||
<UiDateInput v-model="shipmentDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="shipmentDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-customer.name>
|
||||
<UiTextInput
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
@row-click="goToShipment"
|
||||
>
|
||||
<template #header-shipmentDate>
|
||||
<UiDateInput v-model="shipmentDateFilter" size="compact" />
|
||||
<UiDateMaskedInput v-model="shipmentDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-customer.name>
|
||||
<UiTextInput
|
||||
@@ -134,7 +134,7 @@ const shipmentDateFilter = computed<string>({
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ key: 'shipmentDate', label: 'Date et heure', width: '120px' },
|
||||
{ key: 'shipmentDate', label: 'Date', width: '120px' },
|
||||
{ key: 'customer.name', label: 'Client', width: '1.5fr' },
|
||||
{ key: 'address.fullAddress', label: 'Adresse', width: '2fr' },
|
||||
{ key: 'shipmentType.label', label: "Type d'expé.", width: '1.1fr' },
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export interface BovineBuildingCaseRef {
|
||||
caseNumber: number | null
|
||||
building: { label: string } | null
|
||||
}
|
||||
|
||||
export interface BovineData {
|
||||
id: number
|
||||
nationalNumber: string
|
||||
receivedWeight: number | null
|
||||
arrivalDate: string | null
|
||||
buildingCase: string | null
|
||||
exitDate: string | null
|
||||
buildingCase: BovineBuildingCaseRef | null
|
||||
supplier: string | null
|
||||
workNumber: string | null
|
||||
birthDate: string | null
|
||||
|
||||
Reference in New Issue
Block a user