[#313] Création d'une page d'administration : modification/création d'un fournisseur (!20)

| 313| Création d'une page d'administration : modification/création d'un fournisseur |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [ ] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Ferme/pulls/20
Reviewed-by: Autin <tristan@yuno.malio.fr>
Co-authored-by: Matteo <matteo@yuno.malio.fr>
Co-committed-by: Matteo <matteo@yuno.malio.fr>
This commit is contained in:
Matteo
2026-02-12 08:22:17 +00:00
committed by Mattéo Dunoyer
parent 579bdba65b
commit d527e94bac
12 changed files with 589 additions and 56 deletions
+194
View File
@@ -0,0 +1,194 @@
<template>
<form @submit.prevent="validate">
<div class="flex items-center justify-between gap-10">
<h1 class="text-3xl font-bold uppercase">
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
</h1>
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
type="submit"
:disabled="isLoading || !auth.isAdmin"
>
{{ supplierId ? "Sauvegarder" : "Ajouter" }}
</button>
</div>
<div class="grid grid-cols-2 gap-y-16 gap-x-12 mb-10 py-12 border-b border-black ">
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin"/>
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin"/>
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin"/>
</div>
<div class="flex items-center justify-between mb-4 py-6 border-t border-black"></div>
<div class="flex items-center justify-between mb-4">
<h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2>
<button
type="button"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
:disabled="supplierId === null || !auth.isAdmin"
@click="goToAddAddress"
>
Ajouter
</button>
</div>
<div class="overflow-x-auto mb-10">
<table class="w-full border-collapse">
<thead>
<tr class="text-left border-b border-gray-200">
<th class="py-3 pr-4 text-sm uppercase">Libellé</th>
<th class="py-3 pr-4 text-sm uppercase">Rue</th>
<th class="py-3 pr-4 text-sm uppercase">Complément</th>
<th class="py-3 pr-4 text-sm uppercase">Code postal</th>
<th class="py-3 pr-4 text-sm uppercase">Ville</th>
<th class="py-3 pr-4 text-sm uppercase">Pays</th>
</tr>
</thead>
<tbody>
<template v-if="form.addresses.length === 0">
<tr>
<td colspan="6" class="py-4 text-slate-400">
Aucune adresse.
</td>
</tr>
</template>
<template v-else>
<tr
v-for="(address, index) in form.addresses"
:key="address.id ?? index"
class="border-b border-gray-100 hover:bg-slate-50"
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
@click="goToEditAddress(address.id ?? null)"
>
<td class="py-3 pr-4">{{ address.label || "—" }}</td>
<td class="py-3 pr-4">{{ address.street || "—" }}</td>
<td class="py-3 pr-4">{{ address.street2 || "—" }}</td>
<td class="py-3 pr-4">{{ address.postalCode || "—" }}</td>
<td class="py-3 pr-4">{{ address.city || "—" }}</td>
<td class="py-3 pr-4">{{ address.countryCode || "—" }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</form>
</template>
<script setup lang="ts">
import {computed, reactive, ref, watch} from "vue"
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
import {useAuthStore} from "~/stores/auth"
definePageMeta({layout: "admin"})
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const resolveId = (param: unknown) => {
const idStr = Array.isArray(param) ? param[0] : param
if (!idStr) return null
const id = Number(idStr)
return Number.isFinite(id) ? id : null
}
const supplierId = computed(() => resolveId(route.params.id))
const isLoading = ref(false)
const form = reactive<SupplierFormData>({
name: "",
email: "",
phone: "",
addresses: [],
})
const goToAddAddress = () => {
if (supplierId.value === null || !auth.isAdmin) return
router.push({
path: "/admin/supplier/address",
query: {
supplierId: String(supplierId.value),
fromSupplier: "1",
},
})
}
const goToEditAddress = (addressId: number | null) => {
if (supplierId.value === null || addressId === null || !auth.isAdmin) return
router.push({
path: "/admin/supplier/address",
query: {
supplierId: String(supplierId.value),
addressId: String(addressId),
fromSupplier: "1",
},
})
}
const hydrateFromSupplier = (supplier: SupplierData | null) => {
if (!supplier) return
form.name = supplier.name ?? ""
form.email = supplier.email ?? ""
form.phone = supplier.phone ?? ""
if (!Array.isArray(supplier.addresses) || supplier.addresses.length === 0) {
form.addresses = []
return
}
if (typeof supplier.addresses[0] === "string") {
form.addresses = []
return
}
form.addresses = supplier.addresses.map((address) => ({
id: address.id ?? null,
label: address.label ?? "",
street: address.street ?? "",
street2: address.street2 ?? null,
postalCode: address.postalCode ?? "",
city: address.city ?? "",
countryCode: address.countryCode ?? "",
}))
}
watch(
() => supplierId.value,
async (id) => {
if (id === null) return
isLoading.value = true
try {
const supplier = await getSupplier(id)
hydrateFromSupplier(supplier)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
async function validate() {
if (isLoading.value) return
if (!auth.isAdmin) return
isLoading.value = true
try {
const name = form.name.trim()
const email = (form.email ?? "").trim() || null
const phone = (form.phone ?? "").trim() || null
const supplierPayload: SupplierPayload = {
name,
email,
phone,
}
if (supplierId.value !== null) {
await updateSupplier(supplierId.value, supplierPayload)
} else {
await createSupplier(supplierPayload)
}
await router.push("/admin/supplier/supplier-list")
} finally {
isLoading.value = false
}
}
</script>
+47
View File
@@ -0,0 +1,47 @@
<template>
<Address type="supplier" :address="address" @validate="validate"/>
</template>
<script setup lang="ts">
import type {AddressData, AddressPayload} from "~/services/address";
import {createAddress, getAddress, updateAddress} from "~/services/address";
import {getSupplier, updateSupplier} from "~/services/supplier";
import type {SupplierData} from "~/services/dto/supplier-data";
definePageMeta({ layout: "admin" })
const route = useRoute()
const router = useRouter()
const supplierId = computed(() => { return Number(route.query.supplierId) })
const supplier = ref<SupplierData|null>(null);
const addressId = computed(() => { return route.query.addressId !== undefined ? Number(route.query.addressId) : null })
const address = ref<AddressData|null>(null)
const validate = async (address: AddressPayload) => {
try {
if (addressId.value !== null) {
await updateAddress(addressId.value, address)
} else {
await addAddress(address)
}
} finally {
await router.push('/admin/supplier/' + supplierId.value)
}
}
const addAddress = async (address: AddressPayload) => {
const response: AddressData = await createAddress(address)
const addressIRI = `/api/addresses/${response.id}`
const existingIris = (supplier.value.addresses ?? []).map((item: any) => `/api/addresses/${item.id}`)
const next = [...new Set([...existingIris, addressIRI])]
return await updateSupplier(supplierId.value, { addresses: next })
}
onMounted(async () => {
supplier.value = await getSupplier(supplierId.value)
if (addressId.value !== null) {
address.value = await getAddress(addressId.value)
}
})
</script>
+68 -30
View File
@@ -1,16 +1,20 @@
<template>
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase"> Fournisseurs </h1>
<NuxtLink to="/admin/supplier"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
<h1 class="text-3xl font-bold uppercase">Fournisseurs</h1>
<NuxtLink
to="/admin/supplier"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
@click="handleAddClick"
>
Ajouter
</NuxtLink>
</div>
<div class="mt-6 border border-slate-200 mb-16">
<div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16">
<div class="max-h-96 overflow-y-auto">
<div
class="sticky top-0 z-10 grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
class="sticky top-0 z-10 grid grid-cols-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
>
<div>Nom</div>
<div>Mail</div>
@@ -18,57 +22,91 @@
<div>Complément</div>
<div>Code Postal</div>
<div>Ville</div>
<div>Pays</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">
<template v-if="supplier.addresses?.length">
<div
v-if="!supplier.addresses || supplier.addresses.length === 0"
class="grid grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
@click="goToSupplier(supplier.id)"
>
<div class="truncate">{{ supplier.name }}</div>
<div class="truncate">{{ supplier.email }}</div>
<div class="col-span-1">Pas d'adresse</div>
<div class="uppercase truncate">{{"—"}}</div>
<div class="uppercase truncate">{{"—"}}</div>
<div class="uppercase truncate">{{"—"}}</div>
<div class="uppercase truncate">{{"—"}}</div>
</div>
<template v-else-if="supplier.addresses.length > 0">
<div
v-for="addr in supplier.addresses"
:key="addr.id"
class="grid grid-cols-6 hover:bg-slate-50 border-t gap-4 px-4 py-2"
v-for="(address, idx) in supplier.addresses"
:key="address.id ?? `${supplier.id}-${idx}-${address.street}-${address.postalCode}`"
class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
@click="goToSupplier(supplier.id)"
>
<div class="truncate">
{{ supplier.name }}
{{ idx === 0 ? supplier.name : "" }}
</div>
<div class="truncate">
{{ supplier.email }}
</div>
<div class="truncate">
{{ addr.street }}
</div>
<div class="truncate">
{{ addr.street2 }}
</div>
<div>{{ addr.postalCode }}</div>
<div class="uppercase truncate">
{{ addr.city }}
<div class="truncate">{{ idx === 0 ? supplier.email : "" }}</div>
<div class="truncate">{{ address.street || "" }}</div>
<div class="truncate">{{ address.street2 || "" }}</div>
<div>{{ address.postalCode || "" }}</div>
<div class="uppercase truncate">{{ address.city || "" }}</div>
<div class="uppercase truncate">{{ address.countryCode || "" }}</div>
</div>
</template>
<template v-else>
<div
class="grid grid-cols-7 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.email }}</div>
<div class="col-span-5 text-slate-400">
Adresses non chargées
</div>
</div>
</template>
</div>
</div>
</div>
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
</template>
<script setup lang="ts">
import type {SupplierData} from "~/services/dto/supplier-data"
import {getSupplierList} from "~/services/supplier"
import { getSupplierList } from "~/services/supplier"
import type { SupplierData } from "~/services/dto/supplier-data"
import { useAuthStore } from "~/stores/auth"
definePageMeta({layout: "admin"})
definePageMeta({ layout: "admin" })
const supplierList = ref<SupplierData[]>([])
const router = useRouter()
const auth = useAuthStore()
const goToSupplier = (id: number) => {
if (!auth.isAdmin) return
router.push(`/admin/supplier/${id}`)
}
const handleAddClick = (event: Event) => {
if (auth.isAdmin) return
event.preventDefault()
}
onMounted(async () => {
supplierList.value = (await getSupplierList(false)) ?? []
if (!auth.isAdmin) return
supplierList.value = await getSupplierList()
})
</script>