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 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";

View File

@ -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>

View File

@ -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>

View File

@ -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(", ") : "-";

View File

@ -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>

View File

@ -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"

View File

@ -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" />

View File

@ -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>