2026-04-22 10:17:47 +03:00

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>