fix modal
This commit is contained in:
parent
99d88ca30b
commit
2a1de6f384
@ -168,11 +168,11 @@ const getInitials = (name) => {
|
||||
|
||||
const formatAddress = (client) => {
|
||||
const parts = [
|
||||
client.billing_address_line1,
|
||||
client.billing_address_line2,
|
||||
client.billing_postal_code,
|
||||
client.billing_city,
|
||||
client.billing_country_code,
|
||||
client.billing_address.line1,
|
||||
client.billing_address.line2,
|
||||
client.billing_address.postal_code,
|
||||
client.billing_address.city,
|
||||
client.billing_address.country_code,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : "Aucune adresse renseignée";
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<table-action />
|
||||
</template>
|
||||
<template #contact-table>
|
||||
<contact-table :data="contacts" />
|
||||
<contact-table :data="contacts" :loading="loading" @delete="handleDelete" />
|
||||
</template>
|
||||
</contact-template>s
|
||||
</template>
|
||||
@ -20,16 +20,21 @@ import ContactTable from "@/components/molecules/Tables/ContactTable.vue";
|
||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||
import { defineProps } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const emit = defineEmits(["delete"]);
|
||||
|
||||
defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const goToCreateContact = () => {
|
||||
@ -37,4 +42,8 @@ const goToCreateContact = () => {
|
||||
name: "Add Contact",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (contactId) => {
|
||||
emit("delete", contactId);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<i :class="icon" class="me-2"></i>
|
||||
<span class="text-sm">{{ label }}</span>
|
||||
<span class="text-sm" style="margin-right: 10px">{{ label }}</span>
|
||||
<span v-if="badge" class="badge badge-sm bg-gradient-success ms-auto">
|
||||
{{ badge }}
|
||||
</span>
|
||||
|
||||
@ -65,12 +65,16 @@
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-xs font-weight-bold mb-0">
|
||||
{{ location.gps_lat ? Number(location.gps_lat).toFixed(6) : "-" }}
|
||||
{{
|
||||
location.gps_lat ? Number(location.gps_lat).toFixed(6) : "-"
|
||||
}}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-xs font-weight-bold mb-0">
|
||||
{{ location.gps_lng ? Number(location.gps_lng).toFixed(6) : "-" }}
|
||||
{{
|
||||
location.gps_lng ? Number(location.gps_lng).toFixed(6) : "-"
|
||||
}}
|
||||
</p>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@ -114,10 +118,10 @@
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="text-center py-5">
|
||||
<i class="fas fa-map-marked-alt fa-3x text-secondary opacity-5 mb-3"></i>
|
||||
<p class="text-sm text-secondary">
|
||||
Aucune localisation pour ce client
|
||||
</p>
|
||||
<i
|
||||
class="fas fa-map-marked-alt fa-3x text-secondary opacity-5 mb-3"
|
||||
></i>
|
||||
<p class="text-sm text-secondary">Aucune localisation pour ce client</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -167,10 +171,10 @@ const handleRemoveLocation = (locationId) => {
|
||||
|
||||
const formatAddress = (location) => {
|
||||
const parts = [
|
||||
location.address_line1,
|
||||
location.address_line2,
|
||||
location.postal_code,
|
||||
location.city,
|
||||
location.address.line1,
|
||||
location.address.line2,
|
||||
location.address.postal_code,
|
||||
location.address.city,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : "-";
|
||||
|
||||
@ -5,8 +5,13 @@
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
style="background-color: rgba(0, 0, 0, 0.5)"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div
|
||||
class="modal-dialog modal-dialog-centered modal-lg"
|
||||
role="document"
|
||||
@click.stop
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter une localisation</h5>
|
||||
@ -15,20 +20,34 @@
|
||||
class="btn-close"
|
||||
@click="closeModal"
|
||||
aria-label="Close"
|
||||
:disabled="locationIsLoading"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Error Alert -->
|
||||
<div v-if="generalError" class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
{{ generalError }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Nom de la localisation*</label>
|
||||
<label class="form-label">Nom de la localisation</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.name }"
|
||||
placeholder="Ex: Siège social"
|
||||
required
|
||||
maxlength="191"
|
||||
/>
|
||||
<div v-if="errors.name" class="invalid-feedback">
|
||||
{{ errors.name }}
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
Optionnel - maximum 191 caractères
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Type de localisation</label>
|
||||
@ -42,14 +61,21 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Adresse ligne 1*</label>
|
||||
<label class="form-label">Adresse ligne 1</label>
|
||||
<input
|
||||
v-model="formData.address_line1"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.address_line1 }"
|
||||
placeholder="Numéro et nom de rue"
|
||||
required
|
||||
maxlength="255"
|
||||
/>
|
||||
<div v-if="errors.address_line1" class="invalid-feedback">
|
||||
{{ errors.address_line1 }}
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
Optionnel - maximum 255 caractères
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -58,43 +84,77 @@
|
||||
v-model="formData.address_line2"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.address_line2 }"
|
||||
placeholder="Complément d'adresse"
|
||||
maxlength="255"
|
||||
/>
|
||||
<div v-if="errors.address_line2" class="invalid-feedback">
|
||||
{{ errors.address_line2 }}
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
Optionnel - maximum 255 caractères
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Code postal*</label>
|
||||
<label class="form-label">Code postal</label>
|
||||
<input
|
||||
v-model="formData.postal_code"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.postal_code }"
|
||||
placeholder="Ex: 75001"
|
||||
required
|
||||
maxlength="20"
|
||||
/>
|
||||
<div v-if="errors.postal_code" class="invalid-feedback">
|
||||
{{ errors.postal_code }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Ville*</label>
|
||||
<label class="form-label">Ville</label>
|
||||
<input
|
||||
v-model="formData.city"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.city }"
|
||||
placeholder="Ex: Paris"
|
||||
required
|
||||
maxlength="191"
|
||||
/>
|
||||
<div v-if="errors.city" class="invalid-feedback">
|
||||
{{ errors.city }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Pays*</label>
|
||||
<label class="form-label">Pays</label>
|
||||
<input
|
||||
v-model="formData.country_code"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.country_code }"
|
||||
placeholder="Ex: FR"
|
||||
required
|
||||
maxlength="2"
|
||||
/>
|
||||
<div v-if="errors.country_code" class="invalid-feedback">
|
||||
{{ errors.country_code }}
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
2 caractères (ex: FR, BE, DE)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Validation Alert -->
|
||||
<div
|
||||
v-if="showAddressWarning"
|
||||
class="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Au moins un champ d'adresse (adresse, code postal ou ville) doit
|
||||
être renseigné.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Téléphone</label>
|
||||
@ -126,8 +186,13 @@
|
||||
min="-90"
|
||||
max="90"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.gps_lat }"
|
||||
placeholder="Ex: 48.856614"
|
||||
/>
|
||||
<div v-if="errors.gps_lat" class="invalid-feedback">
|
||||
{{ errors.gps_lat }}
|
||||
</div>
|
||||
<div class="form-text text-muted">Entre -90 et 90</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">GPS Longitude</label>
|
||||
@ -138,8 +203,13 @@
|
||||
min="-180"
|
||||
max="180"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': errors.gps_lng }"
|
||||
placeholder="Ex: 2.352222"
|
||||
/>
|
||||
<div v-if="errors.gps_lng" class="invalid-feedback">
|
||||
{{ errors.gps_lng }}
|
||||
</div>
|
||||
<div class="form-text text-muted">Entre -180 et 180</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -149,10 +219,14 @@
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="isDefaultCheckbox"
|
||||
:class="{ 'is-invalid': errors.is_default }"
|
||||
/>
|
||||
<label class="form-check-label" for="isDefaultCheckbox">
|
||||
Définir comme localisation par défaut
|
||||
</label>
|
||||
<div v-if="errors.is_default" class="invalid-feedback d-block">
|
||||
{{ errors.is_default }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -169,13 +243,13 @@
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="locationIsLoading"
|
||||
:disabled="locationIsLoading || !isFormValid"
|
||||
>
|
||||
<span
|
||||
v-if="locationIsLoading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
></span>
|
||||
Ajouter
|
||||
{{ locationIsLoading ? "Création..." : "Ajouter" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -184,8 +258,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, watch, computed, nextTick } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
@ -199,9 +274,13 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "location-created"]);
|
||||
const emit = defineEmits(["close", "location-created", "clear-errors"]);
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
@ -219,6 +298,25 @@ const formData = ref({
|
||||
client_id: props.clientId,
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const showAddressWarning = computed(() => {
|
||||
return (
|
||||
!formData.value.address_line1 &&
|
||||
!formData.value.postal_code &&
|
||||
!formData.value.city
|
||||
);
|
||||
});
|
||||
|
||||
const generalError = computed(() => {
|
||||
return props.errors.general || props.errors.client_id;
|
||||
});
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
// At least one address field should be filled
|
||||
return !showAddressWarning.value;
|
||||
});
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => props.clientId,
|
||||
(newVal) => {
|
||||
@ -226,6 +324,17 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.isVisible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// Clear errors when modal opens
|
||||
emit("clear-errors");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Methods
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: "",
|
||||
@ -242,6 +351,7 @@ const resetForm = () => {
|
||||
is_default: false,
|
||||
client_id: props.clientId,
|
||||
};
|
||||
emit("clear-errors");
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
@ -249,9 +359,67 @@ const closeModal = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit("location-created", formData.value);
|
||||
resetForm();
|
||||
const handleBackdropClick = (event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate GPS coordinates if provided
|
||||
if (formData.value.gps_lat !== null) {
|
||||
const lat = parseFloat(formData.value.gps_lat);
|
||||
if (isNaN(lat) || lat < -90 || lat > 90) {
|
||||
emit("location-created", {
|
||||
...formData.value,
|
||||
errors: { gps_lat: "La latitude doit être comprise entre -90 et 90." },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (formData.value.gps_lng !== null) {
|
||||
const lng = parseFloat(formData.value.gps_lng);
|
||||
if (isNaN(lng) || lng < -180 || lng > 180) {
|
||||
emit("location-created", {
|
||||
...formData.value,
|
||||
errors: {
|
||||
gps_lng: "La longitude doit être comprise entre -180 et 180.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate country code if provided
|
||||
if (formData.value.country_code && formData.value.country_code.length !== 2) {
|
||||
emit("location-created", {
|
||||
...formData.value,
|
||||
errors: {
|
||||
country_code: "Le code pays doit contenir exactement 2 caractères.",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up data before sending
|
||||
const submitData = {
|
||||
...formData.value,
|
||||
gps_lat: formData.value.gps_lat || null,
|
||||
gps_lng: formData.value.gps_lng || null,
|
||||
name: formData.value.name || null,
|
||||
address_line1: formData.value.address_line1 || null,
|
||||
address_line2: formData.value.address_line2 || null,
|
||||
postal_code: formData.value.postal_code || null,
|
||||
city: formData.value.city || null,
|
||||
country_code: formData.value.country_code || "FR",
|
||||
};
|
||||
|
||||
emit("location-created", submitData);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -270,4 +438,37 @@ const handleSubmit = () => {
|
||||
.form-select {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Smooth transitions for modal */
|
||||
.modal-content {
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
class="navbar-brand font-weight-bolder ms-lg-0 ms-3"
|
||||
:class="darkMode ? 'text-black' : 'text-white'"
|
||||
to="/"
|
||||
>Soft UI Dashboard PRO</router-link
|
||||
>Thanasoft</router-link
|
||||
>
|
||||
<button
|
||||
class="shadow-none navbar-toggler ms-2"
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
></i>
|
||||
<router-link class="m-0 navbar-brand" to="/">
|
||||
<img :src="logo" class="navbar-brand-img h-100" alt="main_logo" />
|
||||
<span class="ms-1 font-weight-bold">Soft UI Dashboard PRO</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<hr class="mt-0 horizontal dark" />
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<contact-presentation :contacts="contactStore.contacts" />
|
||||
<contact-presentation
|
||||
:contacts="contactStore.contacts"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ContactPresentation from "@/components/Organism/CRM/ContactPresentation.vue";
|
||||
@ -11,4 +14,13 @@ const contactStore = useContactStore();
|
||||
onMounted(async () => {
|
||||
contactStore.fetchContacts();
|
||||
});
|
||||
|
||||
const handleDelete = async (contactId) => {
|
||||
try {
|
||||
await contactStore.deleteContact(Number(contactId));
|
||||
await contactStore.fetchContacts();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete contact:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user