feat: ajout du composant datatable sur tous les écrans (!48)
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
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #48
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #48.
This commit is contained in:
2026-04-22 13:25:57 +00:00
committed by Autin
parent b94c3a95be
commit 1b4764878e
25 changed files with 1297 additions and 384 deletions

View File

@@ -1,33 +1,31 @@
<template>
<div class="flex items-center justify-between ">
<div class="flex items-center justify-between">
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des types bovins</h1>
</div>
<div class="mt-7 border border-slate-200 mb-11 ">
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Nom</div>
<div>Code</div>
</div>
<div v-if="!auth.isAdmin" class="px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
<div v-else-if="bovinList.length === 0" class="px-4 py-6 text-slate-400">
Aucun type de bovin.
</div>
<template v-else>
<div
v-for="bovin in bovinList"
:key="bovin.id"
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToBovin(bovin.id)"
@keydown.enter="goToBovin(bovin.id)"
>
<div>{{ bovin.label }}</div>
<div>{{ bovin.code }}</div>
</div>
</template>
<div v-if="auth.isAdmin" class="mt-7 mb-11">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToBovin"
>
<template #header-label>
<UiTextInput v-model="filters.label" placeholder="Nom" size="compact" />
</template>
<template #header-code>
<UiTextInput v-model="filters.code" placeholder="Code" size="compact" />
</template>
</UiDataTable>
</div>
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
<div class="flex justify-center items-center">
<NuxtLink
to="/admin/bovin"
@@ -35,24 +33,37 @@
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
@click="handleAddClick"
>
<Icon name="mdi:plus" size="28" />
<Icon name="mdi:plus" size="28" />
Ajouter
</NuxtLink>
</div>
</template>
<script setup lang="ts">
import { getBovineTypeList } from "~/services/bovine-type"
import type { BovineTypeData } from "~/services/dto/bovine-type-data"
import { useAuthStore } from "~/stores/auth"
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const bovinList = ref<BovineTypeData[]>([])
const router = useRouter()
const auth = useAuthStore()
const goToBovin = (id: number) => {
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<BovineTypeData>(
'bovine_types',
{
label: '',
code: ''
}
)
const columns = [
{ key: 'label', label: 'Nom' },
{ key: 'code', label: 'Code' }
]
const goToBovin = (bovin: BovineTypeData) => {
if (!auth.isAdmin) return
router.push(`/admin/bovin/${id}`)
router.push(`/admin/bovin/${bovin.id}`)
}
const handleAddClick = (event: Event) => {
@@ -60,8 +71,7 @@ const handleAddClick = (event: Event) => {
event.preventDefault()
}
onMounted(async () => {
if (!auth.isAdmin) return
bovinList.value = await getBovineTypeList()
onMounted(() => {
if (auth.isAdmin) reload()
})
</script>

View File

@@ -1,51 +1,62 @@
<template>
<div class="flex items-center justify-between ">
<h1 class="text-4xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
<div class="flex items-center justify-between">
<h1 class="text-4xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
</div>
<div class="mt-7 mb-11">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToCarrier"
>
<template #header-name>
<UiTextInput v-model="filters.name" placeholder="Label" size="compact" />
</template>
<template #header-code>
<UiTextInput v-model="filters.code" placeholder="Code" size="compact" />
</template>
</UiDataTable>
</div>
<div class="mt-7 border border-slate-200 mb-11 ">
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Label</div>
<div>Code</div>
</div>
<div
v-for="carrier in carrierList"
:key="carrier.id"
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToCarrier(carrier.id)"
@keydown.enter="goToCarrier(carrier.id)"
>
<div>{{ carrier.name}}</div>
<div>{{ carrier.code }}</div>
</div>
</div>
<div class="flex justify-center items-center">
<NuxtLink
to="/admin/carrier"
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
>
<Icon name="mdi:plus" size="28" />
<Icon name="mdi:plus" size="28" />
Ajouter
</NuxtLink>
</div>
</template>
<script setup lang="ts">
import type {CarrierData} from "~/services/dto/carrier-data";
import {getCarrierList} from "~/services/carrier";
import type { CarrierData } from '~/services/dto/carrier-data'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const carrierList = ref<CarrierData[]>()
const router = useRouter()
const goToCarrier = (id: number) => {
router.push(`/admin/carrier/${id}`)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<CarrierData>(
'carriers',
{
name: '',
code: ''
}
)
const columns = [
{ key: 'name', label: 'Label' },
{ key: 'code', label: 'Code' }
]
const goToCarrier = (carrier: CarrierData) => {
router.push(`/admin/carrier/${carrier.id}`)
}
onMounted(async () => {
carrierList.value = await getCarrierList(false)
})
onMounted(reload)
</script>

View File

@@ -3,37 +3,35 @@
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des clients</h1>
</div>
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
<div class="max-h-96 overflow-y-auto">
<div
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
>
<div>Nom</div>
<div>Téléphone</div>
<div>Mail</div>
<div>Créé par</div>
</div>
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
Aucun client.
</div>
<div
v-for="customer in customerList"
:key="customer.id"
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
@click="goToCustomer(customer.id)"
>
<div class="truncate">{{ customer.name || "—" }}</div>
<div class="truncate">{{ customer.phone || "—" }}</div>
<div class="truncate">{{ customer.email || "—" }}</div>
<div class="truncate">{{ customer.createdBy?.username || "—" }}</div>
</div>
</div>
<div v-if="auth.isAdmin" class="mt-7 mb-11">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToCustomer"
>
<template #header-name>
<UiTextInput v-model="filters.name" placeholder="Nom" size="compact" />
</template>
<template #header-phone>
<UiTextInput v-model="filters.phone" placeholder="Téléphone" size="compact" />
</template>
<template #header-email>
<UiTextInput v-model="filters.email" placeholder="Mail" size="compact" />
</template>
<template #header-createdBy.username>
<UiTextInput v-model="filters['createdBy.username']" placeholder="Créé par" size="compact" />
</template>
</UiDataTable>
</div>
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
<div class="flex justify-center items-center">
<NuxtLink
to="/admin/customer"
@@ -48,17 +46,34 @@
</template>
<script setup lang="ts">
import { getCustomerList } from "~/services/customer"
import type { CustomerData } from "~/services/dto/customer-data"
import { useAuthStore } from "~/stores/auth"
import type { CustomerData } from '~/services/dto/customer-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const customerList = ref<CustomerData[]>([])
const router = useRouter()
const auth = useAuthStore()
const goToCustomer = (id: number) => {
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<CustomerData>(
'customers',
{
name: '',
phone: '',
email: '',
'createdBy.username': ''
}
)
const columns = [
{ key: 'name', label: 'Nom' },
{ key: 'phone', label: 'Téléphone' },
{ key: 'email', label: 'Mail' },
{ key: 'createdBy.username', label: 'Créé par' }
]
const goToCustomer = (customer: CustomerData) => {
if (!auth.isAdmin) return
router.push(`/admin/customer/${id}`)
router.push(`/admin/customer/${customer.id}`)
}
const handleAddClick = (event: Event) => {
@@ -66,8 +81,7 @@ const handleAddClick = (event: Event) => {
event.preventDefault()
}
onMounted(async () => {
if (!auth.isAdmin) return
customerList.value = await getCustomerList()
onMounted(() => {
if (auth.isAdmin) reload()
})
</script>

View File

@@ -3,37 +3,35 @@
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des fournisseurs</h1>
</div>
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
<div class="max-h-96 overflow-y-auto">
<div
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
>
<div>Nom</div>
<div>Téléphone</div>
<div>Mail</div>
<div>Créé par</div>
</div>
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
Aucun fournisseur.
</div>
<div
v-for="supplier in supplierList"
:key="supplier.id"
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
@click="goToSupplier(supplier.id)"
>
<div class="truncate">{{ supplier.name || "—" }}</div>
<div class="truncate">{{ supplier.phone || "—" }}</div>
<div class="truncate">{{ supplier.email || "—" }}</div>
<div class="truncate">{{ supplier.createdBy?.username || "—" }}</div>
</div>
</div>
<div v-if="auth.isAdmin" class="mt-7 mb-11">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToSupplier"
>
<template #header-name>
<UiTextInput v-model="filters.name" placeholder="Nom" size="compact" />
</template>
<template #header-phone>
<UiTextInput v-model="filters.phone" placeholder="Téléphone" size="compact" />
</template>
<template #header-email>
<UiTextInput v-model="filters.email" placeholder="Mail" size="compact" />
</template>
<template #header-createdBy.username>
<UiTextInput v-model="filters['createdBy.username']" placeholder="Créé par" size="compact" />
</template>
</UiDataTable>
</div>
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
<div class="flex justify-center items-center">
<NuxtLink
to="/admin/supplier"
@@ -48,17 +46,34 @@
</template>
<script setup lang="ts">
import { getSupplierList } from "~/services/supplier"
import type { SupplierData } from "~/services/dto/supplier-data"
import { useAuthStore } from "~/stores/auth"
import type { SupplierData } from '~/services/dto/supplier-data'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const supplierList = ref<SupplierData[]>([])
const router = useRouter()
const auth = useAuthStore()
const goToSupplier = (id: number) => {
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<SupplierData>(
'suppliers',
{
name: '',
phone: '',
email: '',
'createdBy.username': ''
}
)
const columns = [
{ key: 'name', label: 'Nom' },
{ key: 'phone', label: 'Téléphone' },
{ key: 'email', label: 'Mail' },
{ key: 'createdBy.username', label: 'Créé par' }
]
const goToSupplier = (supplier: SupplierData) => {
if (!auth.isAdmin) return
router.push(`/admin/supplier/${id}`)
router.push(`/admin/supplier/${supplier.id}`)
}
const handleAddClick = (event: Event) => {
@@ -66,8 +81,7 @@ const handleAddClick = (event: Event) => {
event.preventDefault()
}
onMounted(async () => {
if (!auth.isAdmin) return
supplierList.value = await getSupplierList()
onMounted(() => {
if (auth.isAdmin) reload()
})
</script>

View File

@@ -3,42 +3,52 @@
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
</div>
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
<div class="grid grid-cols-3 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Utilisateur</div>
<div>Role</div>
<div>Statut</div>
</div>
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
Aucun utilisateur.
</div>
<template v-else>
<div
v-for="user in userList"
:key="user.id"
class="grid grid-cols-3 text-primary-700 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"
@click="goToUser(user.id)"
@keydown.enter="goToUser(user.id)"
>
<div>{{ user.username }}</div>
<div>{{ getRoleLabels(user.roles) }}</div>
<div>
<span
v-if="user.isLocked"
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-red-100 text-red-700"
>Verrouillé</span>
<span
v-else
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-green-100 text-green-700"
>Actif</span>
</div>
</div>
</template>
<div v-if="auth.isAdmin" class="mt-7 mb-11">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToUser"
>
<template #header-username>
<UiTextInput
v-model="filters.username"
placeholder="Utilisateur"
size="compact"
/>
</template>
<template #header-roles>
<UiTextInput :model-value="''" placeholder="Role" size="compact" disabled />
</template>
<template #header-isLocked>
<UiSelect
v-model="filters.isLocked"
placeholder="Statut"
:options="statusOptions"
size="compact"
/>
</template>
<template #cell-roles="{ item }">
{{ getRoleLabels(item.roles) }}
</template>
<template #cell-isLocked="{ item }">
<span
v-if="item.isLocked"
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-red-100 text-red-700"
>Verrouillé</span>
<span
v-else
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-green-100 text-green-700"
>Actif</span>
</template>
</UiDataTable>
</div>
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
Acces reserve aux administrateurs.
Accès réservé aux administrateurs.
</div>
<div class="flex justify-center items-center">
@@ -55,19 +65,43 @@
</template>
<script setup lang="ts">
import type { UserData } from "~/services/dto/user-data"
import { getAdminUsers } from "~/services/auth"
import { ROLE } from "~/utils/constants"
import { useAuthStore } from "~/stores/auth"
import type { UserData } from '~/services/dto/user-data'
import { ROLE } from '~/utils/constants'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const userList = ref<UserData[]>([])
const router = useRouter()
const auth = useAuthStore()
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
const roleLabelByValue = new Map(ROLE.map(role => [role.value, role.label]))
const goToUser = (id: number) => {
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<UserData>(
'admin/users',
{
username: '',
isLocked: ''
}
)
const statusOptions = [
{ value: 'false', label: 'Actif' },
{ value: 'true', label: 'Verrouillé' }
]
const columns = [
{ key: 'username', label: 'Utilisateur' },
{ key: 'roles', label: 'Role' },
{ key: 'isLocked', label: 'Statut', width: '160px' }
]
const getRoleLabels = (roles?: string[]) => {
if (!roles || roles.length === 0) return '---'
return roles.map(role => roleLabelByValue.get(role) ?? role).join(', ')
}
const goToUser = (user: UserData) => {
if (!auth.isAdmin) return
router.push(`/admin/user/${id}`)
router.push(`/admin/user/${user.id}`)
}
const handleAddClick = (event: Event) => {
@@ -75,18 +109,7 @@ const handleAddClick = (event: Event) => {
event.preventDefault()
}
const getRoleLabels = (roles?: string[]) => {
if (!roles || roles.length === 0) {
return '---'
}
return roles
.map((role) => roleLabelByValue.get(role) ?? role)
.join(', ')
}
onMounted(async () => {
if (!auth.isAdmin) return
userList.value = await getAdminUsers()
onMounted(() => {
if (auth.isAdmin) reload()
})
</script>

View File

@@ -33,42 +33,51 @@
</NuxtLink>
</div>
<div class="mt-8 border border-slate-200 mb-16">
<div
class="grid grid-cols-3 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
<div class="mt-8 mb-16">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
:row-clickable="auth.isAdmin"
empty-message="Aucun bovin dans cette case."
@row-click="goToBovine"
>
<div>Numéro national</div>
<div>Poids à l'arrivée (kg)</div>
<div>Date d'arrivée</div>
</div>
<template v-if="bovines.length > 0">
<div
v-for="bovine in bovines"
:key="bovine.id"
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm border-t border-slate-200"
:class="auth.isAdmin ? 'cursor-pointer hover:bg-slate-50' : ''"
:role="auth.isAdmin ? 'button' : undefined"
:tabindex="auth.isAdmin ? 0 : undefined"
@click="goToBovine(bovine.id)"
@keydown.enter="goToBovine(bovine.id)"
>
<div>{{ bovine.nationalNumber }}</div>
<div>{{ bovine.receivedWeight ?? '—' }}</div>
<div>{{ formatDate(bovine.arrivalDate) }}</div>
</div>
</template>
<div
v-else
class="px-4 py-3 text-sm border-t border-slate-200 text-slate-500"
>
Aucun bovin dans cette case.
</div>
<template #header-nationalNumber>
<UiTextInput
v-model="filters.nationalNumber"
placeholder="Numéro national"
size="compact"
/>
</template>
<template #header-receivedWeight>
<UiTextInput
v-model="filters.receivedWeight"
placeholder="Poids (kg)"
size="compact"
/>
</template>
<template #header-arrivalDate>
<UiDateInput v-model="arrivalDateFilter" size="compact" />
</template>
<template #cell-arrivalDate="{ item }">
{{ formatDate(item.arrivalDate) }}
</template>
<template #cell-receivedWeight="{ item }">
{{ item.receivedWeight ?? '—' }}
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
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'
const route = useRoute()
const router = useRouter()
@@ -80,7 +89,44 @@ const caseId = computed(() => Number(route.query.id))
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
const buildingCase = ref<BuildingCaseData | null>(null)
const bovines = computed(() => buildingCase.value?.bovines ?? [])
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<BovineData>(
'bovines',
{
buildingCase: '',
nationalNumber: '',
receivedWeight: '',
'arrivalDate[after]': '',
'arrivalDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const arrivalDateFilter = computed<string>({
get: () => (filters.value['arrivalDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['arrivalDate[after]'] = ''
filters.value['arrivalDate[strictly_before]'] = ''
return
}
filters.value['arrivalDate[after]'] = value
filters.value['arrivalDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'nationalNumber', label: 'Numéro national' },
{ key: 'receivedWeight', label: "Poids à l'arrivée (kg)" },
{ key: 'arrivalDate', label: "Date d'arrivée" }
]
const title = computed(() => {
if (!buildingCase.value) return ''
@@ -114,21 +160,27 @@ const loadCase = async () => {
}
const printCaseReport = async () => {
if (!hasCaseId.value) {
return
}
if (!hasCaseId.value) return
const filename = `tableau_poids_case_${caseId.value}.pdf`
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
}
const goToBovine = (id: number) => {
const goToBovine = (bovine: BovineData) => {
if (!auth.isAdmin) return
router.push({
path: '/infrastructure/bovine',
query: { id: String(id), caseId: String(caseId.value) }
query: { id: String(bovine.id), caseId: String(caseId.value) }
})
}
watch(caseId, loadCase, { immediate: true })
watch(caseId, (id) => {
if (!hasCaseId.value) {
filters.value.buildingCase = ''
buildingCase.value = null
return
}
filters.value.buildingCase = `/api/building_cases/${id}`
loadCase()
reload()
}, { immediate: true })
</script>

View File

@@ -5,41 +5,126 @@
</div>
<div class="px-[86px]">
<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 et heure</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"
@click="goToReception(reception.id)"
<div class="mt-6 mb-16">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToReception"
>
<div>{{ reception.identificationNumber}}</div>
<div>{{ formatDate(reception.receptionDate) }}</div>
<div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div>
<div>{{ formatWeighing(reception) }}</div>
</div>
<template #header-identificationNumber>
<UiTextInput
v-model="filters.identificationNumber"
placeholder="Numéro"
size="compact"
/>
</template>
<template #header-receptionDate>
<UiDateInput
v-model="receptionDateFilter"
size="compact"
/>
</template>
<template #header-supplier.name>
<UiTextInput
v-model="filters['supplier.name']"
placeholder="Fournisseur"
size="compact"
/>
</template>
<template #header-address.fullAddress>
<UiTextInput
:model-value="''"
placeholder="Adresse"
size="compact"
disabled
/>
</template>
<template #header-receptionType.label>
<UiSelect
v-model="filters['receptionType.id']"
placeholder="Type réception"
:options="receptionTypeOptions"
size="compact"
/>
</template>
<template #header-weighing>
<UiTextInput
:model-value="''"
placeholder="Poids"
size="compact"
disabled
/>
</template>
<template #cell-receptionDate="{ item }">
{{ formatDate(item.receptionDate) }}
</template>
<template #cell-weighing="{ item }">
{{ formatWeighing(item) }}
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception";
import type {ShipmentData} from "~/services/dto/shipment-data";
import type { ReceptionData } from '~/services/dto/reception-data'
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
import { getReceptionTypeList } from '~/services/reception-type'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const receptionTypes = ref<ReceptionTypeData[]>([])
const receptionTypeOptions = computed(() =>
receptionTypes.value.map(rt => ({ value: rt.id, label: rt.label }))
)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<ReceptionData>(
'receptions',
{
isValid: true,
'identificationNumber': '',
'supplier.name': '',
'receptionType.id': '',
'receptionDate[after]': '',
'receptionDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const receptionDateFilter = computed<string>({
get: () => (filters.value['receptionDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['receptionDate[after]'] = ''
filters.value['receptionDate[strictly_before]'] = ''
return
}
filters.value['receptionDate[after]'] = value
filters.value['receptionDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
{ key: 'receptionDate', label: 'Date et heure', 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' },
{ key: 'weighing', label: 'Poids', width: '82px' }
]
const formatDate = (date: string | null) => {
if (!date) return '—'
@@ -65,11 +150,12 @@ const formatWeighing = (reception: ReceptionData) => {
return `${gross - tare} kg`
}
const goToReception = (id: number) => {
router.push(`/reception/update/${id}`)
const goToReception = (reception: ReceptionData) => {
router.push(`/reception/update/${reception.id}`)
}
onMounted(async () => {
receptionList.value = await getReceptionList(true)
receptionTypes.value = await getReceptionTypeList()
reload()
})
</script>

View File

@@ -1,43 +1,135 @@
<template>
<WorkflowWaitingList
title="listes des réceptions en attente"
:columns="columns"
:items="receptionList ?? []"
route-prefix="/reception"
:show-actions="auth.isAdmin"
>
<template #cell-receptionDate="{ item }">
{{ formatDate(item.receptionDate) }}
</template>
<template #actions="{ item }">
<Icon
name="mdi:delete-outline"
size="24"
class="cursor-pointer text-red-500 hover:text-red-700"
@click="confirmDelete(item)"
/>
</template>
</WorkflowWaitingList>
<div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
</div>
<div class="px-[86px]">
<div class="mt-6 mb-16">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
:show-actions="auth.isAdmin"
row-clickable
@row-click="goToReception"
>
<template #header-receptionDate>
<UiDateInput v-model="receptionDateFilter" size="compact" />
</template>
<template #header-supplier.name>
<UiTextInput
v-model="filters['supplier.name']"
placeholder="Fournisseur"
size="compact"
/>
</template>
<template #header-address.fullAddress>
<UiTextInput :model-value="''" placeholder="Adresse" size="compact" disabled />
</template>
<template #header-receptionType.label>
<UiSelect
v-model="filters['receptionType.id']"
placeholder="Type réception"
:options="receptionTypeOptions"
size="compact"
/>
</template>
<template #header-carrier.name>
<UiTextInput
v-model="filters['carrier.name']"
placeholder="Transporteur"
size="compact"
/>
</template>
<template #header-licensePlate>
<UiTextInput
v-model="filters['licensePlate']"
placeholder="Immatriculation"
size="compact"
/>
</template>
<template #header-actions>
<UiTextInput :model-value="''" placeholder="Actions" size="compact" disabled />
</template>
<template #cell-receptionDate="{ item }">
{{ formatDate(item.receptionDate) }}
</template>
<template #actions="{ item }">
<Icon
name="mdi:delete-outline"
size="24"
class="cursor-pointer text-red-500 hover:text-red-700"
@click="confirmDelete(item)"
/>
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
import type { ReceptionData } from '~/services/dto/reception-data'
import { getReceptionList, deleteReception } from '~/services/reception'
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
import { deleteReception } from '~/services/reception'
import { getReceptionTypeList } from '~/services/reception-type'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const router = useRouter()
const auth = useAuthStore()
const receptionTypes = ref<ReceptionTypeData[]>([])
const receptionTypeOptions = computed(() =>
receptionTypes.value.map(rt => ({ value: rt.id, label: rt.label }))
)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<ReceptionData>(
'receptions',
{
isValid: false,
'supplier.name': '',
'carrier.name': '',
'licensePlate': '',
'receptionType.id': '',
'receptionDate[after]': '',
'receptionDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const receptionDateFilter = computed<string>({
get: () => (filters.value['receptionDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['receptionDate[after]'] = ''
filters.value['receptionDate[strictly_before]'] = ''
return
}
filters.value['receptionDate[after]'] = value
filters.value['receptionDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'receptionDate', label: 'Date et heure' },
{ key: 'supplier.name', label: 'Fournisseur' },
{ key: 'address.fullAddress', label: 'Adresse' },
{ key: 'receptionType.label', label: 'Type réception' },
{ key: 'receptionDate', label: 'Date et heure', 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' },
{ key: 'carrier.name', label: 'Transporteur' },
{ key: 'licensePlate', label: 'Immatriculation' }
{ key: 'licensePlate', label: 'Immatriculation', width: '110px' }
]
const receptionList = ref<ReceptionData[]>()
const formatDate = (date: string | null) => {
if (!date) return '—'
const d = new Date(date.replace(' ', 'T'))
@@ -51,6 +143,10 @@ const formatDate = (date: string | null) => {
})
}
const goToReception = (reception: ReceptionData) => {
router.push(`/reception/${reception.id}`)
}
const confirmDelete = async (reception: ReceptionData) => {
const confirmed = window.confirm(
`Êtes-vous sûr de vouloir supprimer la réception ${reception.identificationNumber ?? `#${reception.id}`} ? Toutes les données liées seront supprimées.`
@@ -58,10 +154,11 @@ const confirmDelete = async (reception: ReceptionData) => {
if (!confirmed) return
await deleteReception(reception.id)
receptionList.value = receptionList.value?.filter(r => r.id !== reception.id)
reload()
}
onMounted(async () => {
receptionList.value = await getReceptionList(false)
receptionTypes.value = await getReceptionTypeList()
reload()
})
</script>

View File

@@ -5,51 +5,148 @@
</div>
<div class="px-[86px]">
<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>Client</div>
<div>Adresse</div>
<div>Type d'expéditon</div>
<div>Poids</div>
</div>
<div
v-for="shipment in shipmentList"
:key="shipment
.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="goShipment(shipment.id)"
<div class="mt-6 mb-16">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
row-clickable
@row-click="goToShipment"
>
<div>{{ shipment.identificationNumber }}</div>
<div>{{ shipment.shipmentDate }}</div>
<div>{{ shipment.customer?.name }}</div>
<div>{{ shipment.address?.fullAddress }}</div>
<div>
<template v-if="formatShipmentLines(shipment).length">
<template #header-identificationNumber>
<UiTextInput
v-model="filters.identificationNumber"
placeholder="Numéro"
size="compact"
/>
</template>
<template #header-shipmentDate>
<UiDateInput v-model="shipmentDateFilter" size="compact" />
</template>
<template #header-customer.name>
<UiTextInput
v-model="filters['customer.name']"
placeholder="Client"
size="compact"
/>
</template>
<template #header-address.fullAddress>
<UiTextInput :model-value="''" placeholder="Adresse" size="compact" disabled />
</template>
<template #header-shipmentType.label>
<UiSelect
v-model="filters['shipmentType.id']"
placeholder="Type d'expédition"
:options="shipmentTypeOptions"
size="compact"
/>
</template>
<template #header-weighing>
<UiTextInput :model-value="''" placeholder="Poids" size="compact" disabled />
</template>
<template #cell-shipmentDate="{ item }">
{{ formatDate(item.shipmentDate) }}
</template>
<template #cell-shipmentType.label="{ item }">
<template v-if="formatShipmentLines(item).length">
<div
v-for="(line, index) in formatShipmentLines(shipment)"
v-for="(line, index) in formatShipmentLines(item)"
:key="index"
class="leading-5"
>
{{ line }}
</div>
</template>
</div>
<div>{{ formatWeighing(shipment) }}</div>
</div>
<template v-else></template>
</template>
<template #cell-weighing="{ item }">
{{ formatWeighing(item) }}
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
import type {ShipmentData} from "~/services/dto/shipment-data";
import {getShipmentList} from "~/services/shipment";
import type { ShipmentData } from '~/services/dto/shipment-data'
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
import { getShipmentTypeList } from '~/services/shipment-type'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const shipmentList = ref<ShipmentData[]>()
const router = useRouter()
const shipmentTypes = ref<ShipmentTypeData[]>([])
const shipmentTypeOptions = computed(() =>
shipmentTypes.value.map(st => ({ value: st.id, label: st.label }))
)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<ShipmentData>(
'shipments',
{
isValid: true,
'identificationNumber': '',
'customer.name': '',
'shipmentType.id': '',
'shipmentDate[after]': '',
'shipmentDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const shipmentDateFilter = computed<string>({
get: () => (filters.value['shipmentDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['shipmentDate[after]'] = ''
filters.value['shipmentDate[strictly_before]'] = ''
return
}
filters.value['shipmentDate[after]'] = value
filters.value['shipmentDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
{ 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édition", width: '1.1fr' },
{ key: 'weighing', label: 'Poids', width: '82px' }
]
const formatDate = (date: string | null) => {
if (!date) return '—'
const d = new Date(date.replace(' ', 'T'))
if (isNaN(d.getTime())) return date
return d.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const formatShipmentLines = (shipment: ShipmentData) => {
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return []
}
const label = typeof shipment.shipmentType === 'string'
? shipment.shipmentType
: shipment.shipmentType?.label
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
}
const formatWeighing = (shipment: ShipmentData) => {
const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
@@ -62,24 +159,12 @@ const formatWeighing = (shipment: ShipmentData) => {
return `${gross - tare} kg`
}
const formatShipmentLines = (shipment: ShipmentData) => {
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return []
}
const label = typeof shipment.shipmentType === 'string'
? shipment.shipmentType
: shipment.shipmentType?.label
return [`${label ?? ''} : ${shipment.nbBovinSend ?? ''}`]
}
const goShipment = (id: number) => {
router.push(`/shipment/update/${id}`)
const goToShipment = (shipment: ShipmentData) => {
router.push(`/shipment/update/${shipment.id}`)
}
onMounted(async () => {
shipmentList.value = await getShipmentList(true)
shipmentTypes.value = await getShipmentTypeList()
reload()
})
</script>

View File

@@ -1,55 +1,147 @@
<template>
<WorkflowWaitingList
title="listes des expéditions en attente"
:columns="columns"
:items="shipmentList ?? []"
route-prefix="/shipment"
:show-actions="auth.isAdmin"
>
<template #cell-shipmentDate="{ item }">
{{ formatDate(item.shipmentDate) }}
</template>
<template #cell-shipmentType="{ item }">
<template v-if="formatShipmentLines(item).length">
<div
v-for="(line, index) in formatShipmentLines(item)"
:key="index"
class="leading-5"
>
{{ line }}
</div>
</template>
<template v-else></template>
</template>
<template #actions="{ item }">
<Icon
name="mdi:delete-outline"
size="24"
class="cursor-pointer text-red-500 hover:text-red-700"
@click="confirmDelete(item)"
/>
</template>
</WorkflowWaitingList>
<div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
</div>
<div class="px-[86px]">
<div class="mt-6 mb-16">
<UiDataTable
v-model:page="page"
v-model:per-page="perPage"
:columns="columns"
:items="items"
:total-items="totalItems"
:loading="loading"
:show-actions="auth.isAdmin"
row-clickable
@row-click="goToShipment"
>
<template #header-shipmentDate>
<UiDateInput v-model="shipmentDateFilter" size="compact" />
</template>
<template #header-customer.name>
<UiTextInput
v-model="filters['customer.name']"
placeholder="Client"
size="compact"
/>
</template>
<template #header-address.fullAddress>
<UiTextInput :model-value="''" placeholder="Adresse" size="compact" disabled />
</template>
<template #header-shipmentType.label>
<UiSelect
v-model="filters['shipmentType.id']"
placeholder="Type d'expé."
:options="shipmentTypeOptions"
size="compact"
/>
</template>
<template #header-carrier.name>
<UiTextInput
v-model="filters['carrier.name']"
placeholder="Transporteur"
size="compact"
/>
</template>
<template #header-licensePlate>
<UiTextInput
v-model="filters['licensePlate']"
placeholder="Immatriculation"
size="compact"
/>
</template>
<template #header-actions>
<UiTextInput :model-value="''" placeholder="Actions" size="compact" disabled />
</template>
<template #cell-shipmentDate="{ item }">
{{ formatDate(item.shipmentDate) }}
</template>
<template #cell-shipmentType.label="{ item }">
<template v-if="formatShipmentLines(item).length">
<div
v-for="(line, index) in formatShipmentLines(item)"
:key="index"
class="leading-5"
>
{{ line }}
</div>
</template>
<template v-else></template>
</template>
<template #actions="{ item }">
<Icon
name="mdi:delete-outline"
size="24"
class="cursor-pointer text-red-500 hover:text-red-700"
@click="confirmDelete(item)"
/>
</template>
</UiDataTable>
</div>
</div>
</template>
<script setup lang="ts">
import type { ShipmentData } from '~/services/dto/shipment-data'
import { getShipmentList, deleteShipment } from '~/services/shipment'
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
import { deleteShipment } from '~/services/shipment'
import { getShipmentTypeList } from '~/services/shipment-type'
import { useAuthStore } from '~/stores/auth'
import { useDataTableServerState } from '~/composables/useDataTableServerState'
const router = useRouter()
const auth = useAuthStore()
const shipmentTypes = ref<ShipmentTypeData[]>([])
const shipmentTypeOptions = computed(() =>
shipmentTypes.value.map(st => ({ value: st.id, label: st.label }))
)
const { items, totalItems, page, perPage, filters, loading, reload } =
useDataTableServerState<ShipmentData>(
'shipments',
{
isValid: false,
'customer.name': '',
'carrier.name': '',
'licensePlate': '',
'shipmentType.id': '',
'shipmentDate[after]': '',
'shipmentDate[strictly_before]': ''
},
{ initialPerPage: 10 }
)
const addOneDay = (dateString: string): string => {
const [year, month, day] = dateString.split('-').map(Number)
const next = new Date(Date.UTC(year, month - 1, day + 1))
return next.toISOString().slice(0, 10)
}
const shipmentDateFilter = computed<string>({
get: () => (filters.value['shipmentDate[after]'] as string) ?? '',
set: (value: string) => {
if (!value) {
filters.value['shipmentDate[after]'] = ''
filters.value['shipmentDate[strictly_before]'] = ''
return
}
filters.value['shipmentDate[after]'] = value
filters.value['shipmentDate[strictly_before]'] = addOneDay(value)
}
})
const columns = [
{ key: 'shipmentDate', label: 'Date et heure' },
{ key: 'customer.name', label: 'Client' },
{ key: 'address.fullAddress', label: 'Adresse' },
{ key: 'shipmentType', label: "Type d'expé." },
{ key: 'shipmentDate', label: 'Date et heure', 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' },
{ key: 'carrier.name', label: 'Transporteur' },
{ key: 'licensePlate', label: 'Immatriculation' }
{ key: 'licensePlate', label: 'Immatriculation', width: '110px' }
]
const shipmentList = ref<ShipmentData[]>()
const formatDate = (date: string | null) => {
if (!date) return '—'
const d = new Date(date.replace(' ', 'T'))
@@ -73,6 +165,10 @@ const formatShipmentLines = (shipment: ShipmentData) => {
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
}
const goToShipment = (shipment: ShipmentData) => {
router.push(`/shipment/${shipment.id}`)
}
const confirmDelete = async (shipment: ShipmentData) => {
const confirmed = window.confirm(
`Êtes-vous sûr de vouloir supprimer l'expédition ${shipment.identificationNumber ?? `#${shipment.id}`} ? Toutes les données liées seront supprimées.`
@@ -80,10 +176,11 @@ const confirmDelete = async (shipment: ShipmentData) => {
if (!confirmed) return
await deleteShipment(shipment.id)
shipmentList.value = shipmentList.value?.filter(s => s.id !== shipment.id)
reload()
}
onMounted(async () => {
shipmentList.value = await getShipmentList(false)
shipmentTypes.value = await getShipmentTypeList()
reload()
})
</script>