fix modal

This commit is contained in:
Nyavokevin 2025-10-21 18:01:08 +03:00
parent 99d88ca30b
commit 2a1de6f384
8 changed files with 264 additions and 39 deletions

View File

@ -168,11 +168,11 @@ const getInitials = (name) => {
const formatAddress = (client) => { const formatAddress = (client) => {
const parts = [ const parts = [
client.billing_address_line1, client.billing_address.line1,
client.billing_address_line2, client.billing_address.line2,
client.billing_postal_code, client.billing_address.postal_code,
client.billing_city, client.billing_address.city,
client.billing_country_code, client.billing_address.country_code,
].filter(Boolean); ].filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "Aucune adresse renseignée"; return parts.length > 0 ? parts.join(", ") : "Aucune adresse renseignée";

View File

@ -10,7 +10,7 @@
<table-action /> <table-action />
</template> </template>
<template #contact-table> <template #contact-table>
<contact-table :data="contacts" /> <contact-table :data="contacts" :loading="loading" @delete="handleDelete" />
</template> </template>
</contact-template>s </contact-template>s
</template> </template>
@ -20,16 +20,21 @@ import ContactTable from "@/components/molecules/Tables/ContactTable.vue";
import addButton from "@/components/molecules/new-button/addButton.vue"; import addButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { defineProps } from "vue"; import { defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const emit = defineEmits(["delete"]);
defineProps({ defineProps({
contacts: { contacts: {
type: Array, type: Array,
default: [], default: [],
}, },
loading: {
type: Boolean,
default: false,
},
}); });
const goToCreateContact = () => { const goToCreateContact = () => {
@ -37,4 +42,8 @@ const goToCreateContact = () => {
name: "Add Contact", name: "Add Contact",
}); });
}; };
const handleDelete = (contactId) => {
emit("delete", contactId);
};
</script> </script>

View File

@ -7,7 +7,7 @@
@click="$emit('click')" @click="$emit('click')"
> >
<i :class="icon" class="me-2"></i> <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"> <span v-if="badge" class="badge badge-sm bg-gradient-success ms-auto">
{{ badge }} {{ badge }}
</span> </span>

View File

@ -65,12 +65,16 @@
</td> </td>
<td> <td>
<p class="text-xs font-weight-bold mb-0"> <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> </p>
</td> </td>
<td> <td>
<p class="text-xs font-weight-bold mb-0"> <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> </p>
</td> </td>
<td class="align-middle"> <td class="align-middle">
@ -114,10 +118,10 @@
</table> </table>
</div> </div>
<div v-else class="text-center py-5"> <div v-else class="text-center py-5">
<i class="fas fa-map-marked-alt fa-3x text-secondary opacity-5 mb-3"></i> <i
<p class="text-sm text-secondary"> class="fas fa-map-marked-alt fa-3x text-secondary opacity-5 mb-3"
Aucune localisation pour ce client ></i>
</p> <p class="text-sm text-secondary">Aucune localisation pour ce client</p>
</div> </div>
</div> </div>
</div> </div>
@ -167,10 +171,10 @@ const handleRemoveLocation = (locationId) => {
const formatAddress = (location) => { const formatAddress = (location) => {
const parts = [ const parts = [
location.address_line1, location.address.line1,
location.address_line2, location.address.line2,
location.postal_code, location.address.postal_code,
location.city, location.address.city,
].filter(Boolean); ].filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "-"; return parts.length > 0 ? parts.join(", ") : "-";

View File

@ -5,8 +5,13 @@
tabindex="-1" tabindex="-1"
role="dialog" role="dialog"
style="background-color: rgba(0, 0, 0, 0.5)" style="background-color: rgba(0, 0, 0, 0.5)"
@click="handleBackdropClick"
>
<div
class="modal-dialog modal-dialog-centered modal-lg"
role="document"
@click.stop
> >
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Ajouter une localisation</h5> <h5 class="modal-title">Ajouter une localisation</h5>
@ -15,20 +20,34 @@
class="btn-close" class="btn-close"
@click="closeModal" @click="closeModal"
aria-label="Close" aria-label="Close"
:disabled="locationIsLoading"
></button> ></button>
</div> </div>
<div class="modal-body"> <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"> <form @submit.prevent="handleSubmit">
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <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 <input
v-model="formData.name" v-model="formData.name"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.name }"
placeholder="Ex: Siège social" 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>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">Type de localisation</label> <label class="form-label">Type de localisation</label>
@ -42,14 +61,21 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Adresse ligne 1*</label> <label class="form-label">Adresse ligne 1</label>
<input <input
v-model="formData.address_line1" v-model="formData.address_line1"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.address_line1 }"
placeholder="Numéro et nom de rue" 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>
<div class="mb-3"> <div class="mb-3">
@ -58,41 +84,75 @@
v-model="formData.address_line2" v-model="formData.address_line2"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.address_line2 }"
placeholder="Complément d'adresse" 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>
<div class="row"> <div class="row">
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label">Code postal*</label> <label class="form-label">Code postal</label>
<input <input
v-model="formData.postal_code" v-model="formData.postal_code"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.postal_code }"
placeholder="Ex: 75001" placeholder="Ex: 75001"
required maxlength="20"
/> />
<div v-if="errors.postal_code" class="invalid-feedback">
{{ errors.postal_code }}
</div>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label">Ville*</label> <label class="form-label">Ville</label>
<input <input
v-model="formData.city" v-model="formData.city"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.city }"
placeholder="Ex: Paris" placeholder="Ex: Paris"
required maxlength="191"
/> />
<div v-if="errors.city" class="invalid-feedback">
{{ errors.city }}
</div>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label">Pays*</label> <label class="form-label">Pays</label>
<input <input
v-model="formData.country_code" v-model="formData.country_code"
type="text" type="text"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.country_code }"
placeholder="Ex: FR" placeholder="Ex: FR"
required maxlength="2"
/> />
<div v-if="errors.country_code" class="invalid-feedback">
{{ errors.country_code }}
</div> </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>
<div class="row"> <div class="row">
@ -126,8 +186,13 @@
min="-90" min="-90"
max="90" max="90"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.gps_lat }"
placeholder="Ex: 48.856614" 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>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">GPS Longitude</label> <label class="form-label">GPS Longitude</label>
@ -138,8 +203,13 @@
min="-180" min="-180"
max="180" max="180"
class="form-control" class="form-control"
:class="{ 'is-invalid': errors.gps_lng }"
placeholder="Ex: 2.352222" 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>
</div> </div>
@ -149,10 +219,14 @@
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="isDefaultCheckbox" id="isDefaultCheckbox"
:class="{ 'is-invalid': errors.is_default }"
/> />
<label class="form-check-label" for="isDefaultCheckbox"> <label class="form-check-label" for="isDefaultCheckbox">
Définir comme localisation par défaut Définir comme localisation par défaut
</label> </label>
<div v-if="errors.is_default" class="invalid-feedback d-block">
{{ errors.is_default }}
</div>
</div> </div>
</form> </form>
</div> </div>
@ -169,13 +243,13 @@
type="button" type="button"
class="btn btn-primary" class="btn btn-primary"
@click="handleSubmit" @click="handleSubmit"
:disabled="locationIsLoading" :disabled="locationIsLoading || !isFormValid"
> >
<span <span
v-if="locationIsLoading" v-if="locationIsLoading"
class="spinner-border spinner-border-sm me-2" class="spinner-border spinner-border-sm me-2"
></span> ></span>
Ajouter {{ locationIsLoading ? "Création..." : "Ajouter" }}
</button> </button>
</div> </div>
</div> </div>
@ -184,8 +258,9 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from "vue"; import { ref, watch, computed, nextTick } from "vue";
import { defineProps, defineEmits } from "vue"; import { defineProps, defineEmits } from "vue";
const props = defineProps({ const props = defineProps({
isVisible: { isVisible: {
type: Boolean, type: Boolean,
@ -199,9 +274,13 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
errors: {
type: Object,
default: () => ({}),
},
}); });
const emit = defineEmits(["close", "location-created"]); const emit = defineEmits(["close", "location-created", "clear-errors"]);
const formData = ref({ const formData = ref({
name: "", name: "",
@ -219,6 +298,25 @@ const formData = ref({
client_id: props.clientId, 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( watch(
() => props.clientId, () => props.clientId,
(newVal) => { (newVal) => {
@ -226,6 +324,17 @@ watch(
} }
); );
watch(
() => props.isVisible,
(newVal) => {
if (newVal) {
// Clear errors when modal opens
emit("clear-errors");
}
}
);
// Methods
const resetForm = () => { const resetForm = () => {
formData.value = { formData.value = {
name: "", name: "",
@ -242,6 +351,7 @@ const resetForm = () => {
is_default: false, is_default: false,
client_id: props.clientId, client_id: props.clientId,
}; };
emit("clear-errors");
}; };
const closeModal = () => { const closeModal = () => {
@ -249,9 +359,67 @@ const closeModal = () => {
emit("close"); emit("close");
}; };
const handleSubmit = () => { const handleBackdropClick = (event) => {
emit("location-created", formData.value); if (event.target === event.currentTarget) {
resetForm(); 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> </script>
@ -270,4 +438,37 @@ const handleSubmit = () => {
.form-select { .form-select {
font-size: 0.875rem; 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> </style>

View File

@ -9,7 +9,7 @@
class="navbar-brand font-weight-bolder ms-lg-0 ms-3" class="navbar-brand font-weight-bolder ms-lg-0 ms-3"
:class="darkMode ? 'text-black' : 'text-white'" :class="darkMode ? 'text-black' : 'text-white'"
to="/" to="/"
>Soft UI Dashboard PRO</router-link >Thanasoft</router-link
> >
<button <button
class="shadow-none navbar-toggler ms-2" class="shadow-none navbar-toggler ms-2"

View File

@ -12,7 +12,6 @@
></i> ></i>
<router-link class="m-0 navbar-brand" to="/"> <router-link class="m-0 navbar-brand" to="/">
<img :src="logo" class="navbar-brand-img h-100" alt="main_logo" /> <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> </router-link>
</div> </div>
<hr class="mt-0 horizontal dark" /> <hr class="mt-0 horizontal dark" />

View File

@ -1,5 +1,8 @@
<template> <template>
<contact-presentation :contacts="contactStore.contacts" /> <contact-presentation
:contacts="contactStore.contacts"
@delete="handleDelete"
/>
</template> </template>
<script setup> <script setup>
import ContactPresentation from "@/components/Organism/CRM/ContactPresentation.vue"; import ContactPresentation from "@/components/Organism/CRM/ContactPresentation.vue";
@ -11,4 +14,13 @@ const contactStore = useContactStore();
onMounted(async () => { onMounted(async () => {
contactStore.fetchContacts(); 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> </script>