| Numéro du ticket | Titre du ticket | |------------------|-----------------| | #326 | Admin modification creation client | ## 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/25 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:
@@ -44,6 +44,7 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#275] Lister les expéditions en attente
|
* [#275] Lister les expéditions en attente
|
||||||
* [#276] Lister les expéditions terminées
|
* [#276] Lister les expéditions terminées
|
||||||
* [#324] Creation page admin listing clients
|
* [#324] Creation page admin listing clients
|
||||||
|
* [#326] Admin modification creation client
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"create": "Fournisseur créé avec succès.",
|
"create": "Fournisseur créé avec succès.",
|
||||||
"update": "Fournisseur mis à jour avec succès."
|
"update": "Fournisseur mis à jour avec succès."
|
||||||
},
|
},
|
||||||
|
"customer": {
|
||||||
|
"create": "Client créé avec succès.",
|
||||||
|
"update": "Client mis à jour avec succès."
|
||||||
|
},
|
||||||
"address": {
|
"address": {
|
||||||
"create": "Adresse créée avec succès.",
|
"create": "Adresse créée avec succès.",
|
||||||
"update": "Adresse mise à jour avec succès."
|
"update": "Adresse mise à jour avec succès."
|
||||||
|
|||||||
@@ -1,12 +1,192 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
|
{{ customerId ? "Modifications du client" : "Ajout d'un client" }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
{{ customerId ? "Sauvegarder" : "Ajouter" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
|
||||||
|
<UiTextInput id="customer-label" v-model="form.label" label="Nom du client" :disabled="!auth.isAdmin"/>
|
||||||
|
<UiTextInput id="customer-code" v-model="form.code" label="Code" :disabled="!auth.isAdmin"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-24 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 client</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="customerId === 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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||||
|
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
definePageMeta({layout: "admin"})
|
definePageMeta({layout: "admin"})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
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 customerId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const form = reactive<CustomerFormData>({
|
||||||
|
label: "",
|
||||||
|
code: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (customerId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (customerId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromCustomer = (customer: CustomerData | null) => {
|
||||||
|
if (!customer) return
|
||||||
|
form.label = customer.label ?? ""
|
||||||
|
form.code = customer.code ?? ""
|
||||||
|
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof customer.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = customer.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(
|
||||||
|
() => customerId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const customer = await getCustomer(id)
|
||||||
|
hydrateFromCustomer(customer)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const label = form.label.trim()
|
||||||
|
const code = form.code.trim()
|
||||||
|
|
||||||
|
const customerPayload: CustomerPayload = {
|
||||||
|
label,
|
||||||
|
code,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (customerId.value !== null) {
|
||||||
|
await updateCustomer(customerId.value, customerPayload)
|
||||||
|
targetId = customerId.value
|
||||||
|
} else {
|
||||||
|
const created = await createCustomer(customerPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId !== null) {
|
||||||
|
await router.push(`/admin/customer/${targetId}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="customer" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressData, AddressPayload } from "~/services/address"
|
||||||
|
import { createAddress, getAddress, updateAddress } from "~/services/address"
|
||||||
|
import { getCustomer, updateCustomer } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
definePageMeta({ layout: "admin" })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const customerId = computed(() => Number(route.query.customerId))
|
||||||
|
const customer = ref<CustomerData | null>(null)
|
||||||
|
const addressId = computed(() => (route.query.addressId !== undefined ? Number(route.query.addressId) : null))
|
||||||
|
const address = ref<AddressData | null>(null)
|
||||||
|
|
||||||
|
const validate = async (payload: AddressPayload) => {
|
||||||
|
try {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, payload)
|
||||||
|
} else {
|
||||||
|
await addAddress(payload)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await router.push("/admin/customer/" + customerId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (payload: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(payload)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (customer.value?.addresses ?? [])
|
||||||
|
.map((item: any) => (typeof item === "string" ? item : `/api/addresses/${item.id}`))
|
||||||
|
.filter((iri: string | null) => Boolean(iri)) as string[]
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateCustomer(customerId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
customer.value = await getCustomer(customerId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase">Client</h1>
|
<h1 class="text-3xl font-bold uppercase">Liste des Clients</h1>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/customer"
|
to="/admin/customer"
|
||||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
|||||||
@@ -1,23 +1,43 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from "~/composables/useApi"
|
||||||
import type { CustomerData } from '~/services/dto/customer-data'
|
import type { CustomerData, CustomerPayload } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
export type CustomerListResponse =
|
export type CustomerListResponse =
|
||||||
| CustomerData[]
|
| CustomerData[]
|
||||||
| { 'hydra:member'?: CustomerData[] }
|
| { "hydra:member"?: CustomerData[] }
|
||||||
|
|
||||||
export async function getCustomerList(): Promise<CustomerData[]> {
|
export async function getCustomerList(): Promise<CustomerData[]> {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const response = await api.get<CustomerListResponse>('customers', {}, {
|
const response = await api.get<CustomerListResponse>("customers", {}, {
|
||||||
toastErrorKey: 'errors.customer.list'
|
toastErrorKey: "errors.customer.list",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) return response
|
||||||
return response
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
|
||||||
return response['hydra:member']
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCustomer(id: number): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<CustomerData>(`customers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.customer.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomer(id: number, payload: Partial<CustomerPayload>): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<CustomerData>(`customers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.customer.update",
|
||||||
|
toastSuccessKey: "success.customer.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCustomer(payload: CustomerPayload): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<CustomerData>("customers", payload, {
|
||||||
|
toastErrorKey: "errors.customer.create",
|
||||||
|
toastSuccessKey: "success.customer.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import type { AddressData } from "~/services/dto/address-data"
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type CustomerAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
export interface CustomerData {
|
export interface CustomerData {
|
||||||
id: number
|
id: number
|
||||||
label: string
|
label: string
|
||||||
code?: string | null
|
code?: string | null
|
||||||
addresses?: AddressData[] | null
|
addresses: CustomerAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerFormData {
|
||||||
|
label: string
|
||||||
|
code?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerPayload = {
|
||||||
|
label: string
|
||||||
|
code?: string | null
|
||||||
|
addresses?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-5
@@ -8,6 +8,8 @@ use ApiPlatform\Metadata\ApiProperty;
|
|||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@@ -24,6 +26,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['customer:read']],
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
),
|
),
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
|
denormalizationContext: ['groups' => ['customer:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
|
denormalizationContext: ['groups' => ['customer:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
)]
|
)]
|
||||||
@@ -36,11 +48,11 @@ class Customer
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
#[Groups(['customer:read', 'shipment:read'])]
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
private ?string $label = null;
|
private ?string $label = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
#[Groups(['customer:read', 'shipment:read'])]
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
private ?string $code = null;
|
private ?string $code = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +60,7 @@ class Customer
|
|||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
||||||
#[ORM\JoinTable(name: 'customer_address')]
|
#[ORM\JoinTable(name: 'customer_address')]
|
||||||
#[Groups(['customer:read'])]
|
#[Groups(['customer:read', 'customer:write'])]
|
||||||
#[ApiProperty(readableLink: true)]
|
#[ApiProperty(readableLink: true)]
|
||||||
private Collection $addresses;
|
private Collection $addresses;
|
||||||
|
|
||||||
@@ -87,8 +99,29 @@ class Customer
|
|||||||
return $this->addresses;
|
return $this->addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAddresses(Collection $addresses): void
|
public function setAddresses(iterable $addresses): self
|
||||||
{
|
{
|
||||||
$this->addresses = $addresses;
|
$this->addresses->clear();
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$this->addAddress($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAddress(Address $address): self
|
||||||
|
{
|
||||||
|
if (!$this->addresses->contains($address)) {
|
||||||
|
$this->addresses->add($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAddress(Address $address): self
|
||||||
|
{
|
||||||
|
$this->addresses->removeElement($address);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user