677 lines
21 KiB
Vue
677 lines
21 KiB
Vue
<template>
|
|
<div class="card">
|
|
<div class="card-header pb-0">
|
|
<div class="d-flex align-items-center">
|
|
<h6 class="mb-0">Informations détaillées</h6>
|
|
<button
|
|
v-if="!isEditing"
|
|
class="btn btn-primary btn-sm ms-auto"
|
|
@click="startEdit"
|
|
>
|
|
<i class="fas fa-edit me-1"></i>Modifier
|
|
</button>
|
|
<div v-else class="ms-auto">
|
|
<button
|
|
class="btn btn-outline-secondary btn-sm me-2"
|
|
@click="cancelEdit"
|
|
>
|
|
<i class="fas fa-times me-1"></i>Annuler
|
|
</button>
|
|
<button
|
|
class="btn btn-success btn-sm"
|
|
:disabled="isSaving"
|
|
@click="saveChanges"
|
|
>
|
|
<i class="fas fa-save me-1"></i>
|
|
{{ isSaving ? "Enregistrement..." : "Enregistrer" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<form @submit.prevent="saveChanges">
|
|
<!-- Informations générales -->
|
|
<div class="info-section mb-4">
|
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
|
<i class="fas fa-building text-primary me-2"></i>Informations
|
|
générales
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label"
|
|
>Nom du client <span class="text-danger">*</span></label
|
|
>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.name"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.name }"
|
|
placeholder="Nom du client"
|
|
maxlength="255"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.name }}
|
|
</p>
|
|
<div v-if="errors.name" class="invalid-feedback d-block">
|
|
{{ errors.name }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Type</label>
|
|
<p class="form-control-static text-sm">
|
|
{{ client.type_label || "-" }}
|
|
</p>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">SIRET</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.siret"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.siret }"
|
|
placeholder="SIRET"
|
|
maxlength="20"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.siret || "-" }}
|
|
</p>
|
|
<div v-if="errors.siret" class="invalid-feedback d-block">
|
|
{{ errors.siret }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Numéro de TVA</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.vat_number"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.vat_number }"
|
|
placeholder="Numéro de TVA"
|
|
maxlength="32"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.vat_number || "-" }}
|
|
</p>
|
|
<div v-if="errors.vat_number" class="invalid-feedback d-block">
|
|
{{ errors.vat_number }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Groupe de clients</label>
|
|
<select
|
|
v-if="isEditing"
|
|
v-model="formData.group_id"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.group_id }"
|
|
>
|
|
<option :value="null">Aucun groupe</option>
|
|
<option
|
|
v-for="group in clientGroups"
|
|
:key="group.id"
|
|
:value="group.id"
|
|
>
|
|
{{ group.name }}
|
|
</option>
|
|
</select>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ getGroupName(client.group_id) || "-" }}
|
|
</p>
|
|
<div v-if="errors.group_id" class="invalid-feedback d-block">
|
|
{{ errors.group_id }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Taux de TVA par défaut</label>
|
|
<select
|
|
v-if="isEditing"
|
|
v-model="formData.default_tva_rate_id"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.default_tva_rate_id }"
|
|
>
|
|
<option :value="null">Taux par défaut</option>
|
|
<option
|
|
v-for="tvaRate in tvaRates"
|
|
:key="tvaRate.id"
|
|
:value="tvaRate.id"
|
|
>
|
|
{{ tvaRate.rate }}% - {{ tvaRate.name }}
|
|
</option>
|
|
</select>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ getTvaRateName(client.default_tva_rate_id) || "-" }}
|
|
</p>
|
|
<div
|
|
v-if="errors.default_tva_rate_id"
|
|
class="invalid-feedback d-block"
|
|
>
|
|
{{ errors.default_tva_rate_id }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Statut</label>
|
|
<div v-if="isEditing" class="form-check form-switch mt-2">
|
|
<input
|
|
v-model="formData.is_active"
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
:class="{ 'is-invalid': errors.is_active }"
|
|
/>
|
|
<label class="form-check-label">
|
|
{{ formData.is_active ? "Actif" : "Inactif" }}
|
|
</label>
|
|
</div>
|
|
<p v-else class="form-control-static text-sm">
|
|
<span
|
|
:class="client.is_active ? 'text-success' : 'text-danger'"
|
|
>
|
|
{{ client.is_active ? "Actif" : "Inactif" }}
|
|
</span>
|
|
</p>
|
|
<div v-if="errors.is_active" class="invalid-feedback d-block">
|
|
{{ errors.is_active }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact -->
|
|
<div class="info-section mb-4">
|
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
|
<i class="fas fa-phone text-success me-2"></i>Contact
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Email</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.email"
|
|
type="email"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.email }"
|
|
placeholder="email@example.com"
|
|
maxlength="191"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
<a v-if="client.email" :href="`mailto:${client.email}`">
|
|
{{ client.email }}
|
|
</a>
|
|
<span v-else>-</span>
|
|
</p>
|
|
<div v-if="errors.email" class="invalid-feedback d-block">
|
|
{{ errors.email }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Téléphone</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.phone"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.phone }"
|
|
placeholder="+33 1 23 45 67 89"
|
|
maxlength="50"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.phone || "-" }}
|
|
</p>
|
|
<div v-if="errors.phone" class="invalid-feedback d-block">
|
|
{{ errors.phone }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Adresse de facturation -->
|
|
<div class="info-section mb-4">
|
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
|
<i class="fas fa-map-marker-alt text-warning me-2"></i>Adresse de
|
|
facturation
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col-12 mb-3">
|
|
<label class="form-label">Adresse ligne 1</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.billing_address_line1"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.billing_address_line1 }"
|
|
placeholder="Adresse"
|
|
maxlength="255"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.billing_address?.line1 || "-" }}
|
|
</p>
|
|
<div
|
|
v-if="errors.billing_address_line1"
|
|
class="invalid-feedback d-block"
|
|
>
|
|
{{ errors.billing_address_line1 }}
|
|
</div>
|
|
</div>
|
|
<div class="col-12 mb-3">
|
|
<label class="form-label">Adresse ligne 2</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.billing_address_line2"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.billing_address_line2 }"
|
|
placeholder="Complément d'adresse"
|
|
maxlength="255"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.billing_address?.line2 || "-" }}
|
|
</p>
|
|
<div
|
|
v-if="errors.billing_address_line2"
|
|
class="invalid-feedback d-block"
|
|
>
|
|
{{ errors.billing_address_line2 }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Code postal</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.billing_postal_code"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.billing_postal_code }"
|
|
placeholder="75001"
|
|
maxlength="20"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.billing_address?.postal_code || "-" }}
|
|
</p>
|
|
<div
|
|
v-if="errors.billing_postal_code"
|
|
class="invalid-feedback d-block"
|
|
>
|
|
{{ errors.billing_postal_code }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Ville</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.billing_city"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.billing_city }"
|
|
placeholder="Paris"
|
|
maxlength="191"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.billing_address?.city || "-" }}
|
|
</p>
|
|
<div v-if="errors.billing_city" class="invalid-feedback d-block">
|
|
{{ errors.billing_city }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Pays</label>
|
|
<input
|
|
v-if="isEditing"
|
|
v-model="formData.billing_country_code"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.billing_country_code }"
|
|
placeholder="FR"
|
|
maxlength="2"
|
|
/>
|
|
<p v-else class="form-control-static text-sm">
|
|
{{ client.billing_address?.country_code || "-" }}
|
|
</p>
|
|
<div
|
|
v-if="errors.billing_country_code"
|
|
class="invalid-feedback d-block"
|
|
>
|
|
{{ errors.billing_country_code }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="info-section">
|
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
|
<i class="fas fa-sticky-note text-info me-2"></i>Notes
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col-12 mb-3">
|
|
<label class="form-label">Notes internes</label>
|
|
<textarea
|
|
v-if="isEditing"
|
|
v-model="formData.notes"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.notes }"
|
|
rows="4"
|
|
placeholder="Notes..."
|
|
></textarea>
|
|
<p
|
|
v-else
|
|
class="form-control-static text-sm"
|
|
style="white-space: pre-wrap"
|
|
>
|
|
{{ client.notes || "-" }}
|
|
</p>
|
|
<div v-if="errors.notes" class="invalid-feedback d-block">
|
|
{{ errors.notes }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, watch } from "vue";
|
|
import { useClientStore } from "@/stores/clientStore";
|
|
import { defineProps, defineEmits } from "vue";
|
|
const props = defineProps({
|
|
client: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
clientGroups: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
tvaRates: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
});
|
|
|
|
const clientStore = useClientStore();
|
|
const isEditing = ref(false);
|
|
const isSaving = ref(false);
|
|
const errors = reactive({});
|
|
const formData = reactive({
|
|
name: "",
|
|
vat_number: "",
|
|
siret: "",
|
|
email: "",
|
|
phone: "",
|
|
billing_address_line1: "",
|
|
billing_address_line2: "",
|
|
billing_postal_code: "",
|
|
billing_city: "",
|
|
billing_country_code: "",
|
|
group_id: null,
|
|
notes: "",
|
|
is_active: true,
|
|
default_tva_rate_id: null,
|
|
});
|
|
|
|
const startEdit = () => {
|
|
const billingAddress = props.client.billing_address || {};
|
|
|
|
isEditing.value = true;
|
|
Object.assign(formData, {
|
|
name: props.client.name || "",
|
|
vat_number: props.client.vat_number || "",
|
|
siret: props.client.siret || "",
|
|
email: props.client.email || "",
|
|
phone: props.client.phone || "",
|
|
billing_address_line1: billingAddress.line1 || "",
|
|
billing_address_line2: billingAddress.line2 || "",
|
|
billing_postal_code: billingAddress.postal_code || "",
|
|
billing_city: billingAddress.city || "",
|
|
billing_country_code: billingAddress.country_code || "FR",
|
|
group_id: props.client.group_id || null,
|
|
notes: props.client.notes || "",
|
|
is_active:
|
|
props.client.is_active !== undefined ? props.client.is_active : true,
|
|
default_tva_rate_id: props.client.default_tva_rate_id || null,
|
|
});
|
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
|
};
|
|
|
|
const cancelEdit = () => {
|
|
isEditing.value = false;
|
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
|
};
|
|
|
|
const getGroupName = (groupId) => {
|
|
const group = props.clientGroups.find((g) => g.id === groupId);
|
|
return group ? group.name : null;
|
|
};
|
|
|
|
const getTvaRateName = (tvaRateId) => {
|
|
const tvaRate = props.tvaRates.find((t) => t.id === tvaRateId);
|
|
return tvaRate ? `${tvaRate.rate}% - ${tvaRate.name}` : null;
|
|
};
|
|
|
|
const validateForm = () => {
|
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
|
let isValid = true;
|
|
|
|
// Name validation
|
|
if (!formData.name || formData.name.trim() === "") {
|
|
errors.name = "Le nom du client est obligatoire.";
|
|
isValid = false;
|
|
} else if (formData.name.length > 255) {
|
|
errors.name = "Le nom du client ne peut pas dépasser 255 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
// VAT number validation
|
|
if (formData.vat_number && formData.vat_number.length > 32) {
|
|
errors.vat_number = "Le numéro de TVA ne peut pas dépasser 32 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
// SIRET validation
|
|
if (formData.siret && formData.siret.length > 20) {
|
|
errors.siret = "Le SIRET ne peut pas dépasser 20 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
// Email validation
|
|
if (formData.email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(formData.email)) {
|
|
errors.email = "L'adresse email doit être valide.";
|
|
isValid = false;
|
|
} else if (formData.email.length > 191) {
|
|
errors.email = "L'adresse email ne peut pas dépasser 191 caractères.";
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
// Phone validation
|
|
if (formData.phone && formData.phone.length > 50) {
|
|
errors.phone = "Le téléphone ne peut pas dépasser 50 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
// Billing address validations
|
|
if (
|
|
formData.billing_address_line1 &&
|
|
formData.billing_address_line1.length > 255
|
|
) {
|
|
errors.billing_address_line1 =
|
|
"L'adresse ne peut pas dépasser 255 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
if (
|
|
formData.billing_address_line2 &&
|
|
formData.billing_address_line2.length > 255
|
|
) {
|
|
errors.billing_address_line2 =
|
|
"Le complément d'adresse ne peut pas dépasser 255 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
if (
|
|
formData.billing_postal_code &&
|
|
formData.billing_postal_code.length > 20
|
|
) {
|
|
errors.billing_postal_code =
|
|
"Le code postal ne peut pas dépasser 20 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
if (formData.billing_city && formData.billing_city.length > 191) {
|
|
errors.billing_city = "La ville ne peut pas dépasser 191 caractères.";
|
|
isValid = false;
|
|
}
|
|
|
|
// Validation corrigée pour billing_country_code
|
|
if (formData.billing_country_code) {
|
|
if (formData.billing_country_code.length !== 2) {
|
|
errors.billing_country_code = "Le code pays doit contenir 2 caractères.";
|
|
isValid = false;
|
|
}
|
|
} else {
|
|
// Si vide, on met une valeur par défaut pour éviter l'erreur SQL
|
|
formData.billing_country_code = "FR";
|
|
}
|
|
|
|
// Group validation
|
|
if (
|
|
formData.group_id &&
|
|
!props.clientGroups.find((g) => g.id === formData.group_id)
|
|
) {
|
|
errors.group_id = "Le groupe de clients sélectionné n'existe pas.";
|
|
isValid = false;
|
|
}
|
|
|
|
// TVA rate validation
|
|
if (
|
|
formData.default_tva_rate_id &&
|
|
!props.tvaRates.find((t) => t.id === formData.default_tva_rate_id)
|
|
) {
|
|
errors.default_tva_rate_id = "Le taux de TVA sélectionné n'existe pas.";
|
|
isValid = false;
|
|
}
|
|
|
|
// Active status validation
|
|
if (typeof formData.is_active !== "boolean") {
|
|
errors.is_active = "Le statut actif doit être vrai ou faux.";
|
|
isValid = false;
|
|
}
|
|
|
|
return isValid;
|
|
};
|
|
|
|
const prepareFormData = () => {
|
|
// Nettoyer les données avant envoi
|
|
const cleanedData = { ...formData };
|
|
|
|
// Convertir les chaînes vides en null pour les champs nullable
|
|
const nullableFields = [
|
|
"vat_number",
|
|
"siret",
|
|
"email",
|
|
"phone",
|
|
"billing_address_line1",
|
|
"billing_address_line2",
|
|
"billing_postal_code",
|
|
"billing_city",
|
|
"notes",
|
|
"group_id",
|
|
"default_tva_rate_id",
|
|
];
|
|
|
|
nullableFields.forEach((field) => {
|
|
if (cleanedData[field] === "") {
|
|
cleanedData[field] = null;
|
|
}
|
|
});
|
|
|
|
// S'assurer que billing_country_code a une valeur
|
|
if (
|
|
!cleanedData.billing_country_code ||
|
|
cleanedData.billing_country_code === ""
|
|
) {
|
|
cleanedData.billing_country_code = "FR";
|
|
}
|
|
|
|
return cleanedData;
|
|
};
|
|
const saveChanges = async () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
isSaving.value = true;
|
|
|
|
try {
|
|
isEditing.value = false;
|
|
emit("client-updated", prepareFormData());
|
|
} catch (error) {
|
|
console.error("Erreur lors de la mise à jour:", error);
|
|
if (error.response && error.response.data && error.response.data.errors) {
|
|
Object.assign(errors, error.response.data.errors);
|
|
} else {
|
|
errors.general = "Une erreur est survenue lors de la sauvegarde.";
|
|
}
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
};
|
|
|
|
// Émettre les événements
|
|
const emit = defineEmits(["client-updated"]);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-control-static {
|
|
padding-top: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
margin-bottom: 0;
|
|
min-height: calc(1.5em + 1rem);
|
|
border: 1px solid transparent;
|
|
background-color: transparent;
|
|
}
|
|
|
|
.info-section {
|
|
padding: 1rem;
|
|
background-color: #f8f9fa;
|
|
border-radius: 0.5rem;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.form-control:focus {
|
|
border-color: #cb0c9f;
|
|
box-shadow: 0 0 0 0.2rem rgba(203, 12, 159, 0.25);
|
|
}
|
|
|
|
.invalid-feedback {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.form-check-input:checked {
|
|
background-color: #cb0c9f;
|
|
border-color: #cb0c9f;
|
|
}
|
|
|
|
.text-success {
|
|
color: #198754 !important;
|
|
}
|
|
|
|
.text-danger {
|
|
color: #dc3545 !important;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: #cb0c9f;
|
|
border-color: #cb0c9f;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #a90982;
|
|
border-color: #a90982;
|
|
}
|
|
</style>
|