add location
This commit is contained in:
parent
78700a3c5a
commit
b62cb3d717
@ -23,7 +23,8 @@
|
||||
:initials="getInitials(client.name)"
|
||||
:client-name="client.name"
|
||||
:client-type="client.type_label || 'Client'"
|
||||
:contacts-count="client.length"
|
||||
:contacts-count="contacts.length"
|
||||
:locations-count="locations.length"
|
||||
:is-active="client.is_active"
|
||||
:active-tab="activeTab"
|
||||
@edit-avatar="triggerFileInput"
|
||||
@ -44,12 +45,17 @@
|
||||
:active-tab="activeTab"
|
||||
:client="client"
|
||||
:contacts="contacts"
|
||||
:locations="locations"
|
||||
:formatted-address="formatAddress(client)"
|
||||
:client-id="client.id"
|
||||
:contact-is-loading="contactLoading"
|
||||
:location-is-loading="locationLoading"
|
||||
@change-tab="activeTab = $event"
|
||||
@updating-client="handleUpdateClient"
|
||||
@create-contact="handleAddContact"
|
||||
@create-location="handleAddLocation"
|
||||
@modify-location="handleModifyLocation"
|
||||
@remove-location="handleRemoveLocation"
|
||||
/>
|
||||
</template>
|
||||
</client-detail-template>
|
||||
@ -71,6 +77,11 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -91,6 +102,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
locationLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const localAvatar = ref(props.clientAvatar);
|
||||
@ -99,6 +114,9 @@ const emit = defineEmits([
|
||||
"updateTheClient",
|
||||
"handleFileInput",
|
||||
"add-new-contact",
|
||||
"add-new-location",
|
||||
"modify-location",
|
||||
"remove-location",
|
||||
]);
|
||||
|
||||
const handleAvatarUpload = (event) => {
|
||||
@ -123,10 +141,21 @@ const inputFile = () => {
|
||||
};
|
||||
|
||||
const handleAddContact = (data) => {
|
||||
// TODO: Implement add contact functionality
|
||||
emit("add-new-contact", data);
|
||||
};
|
||||
|
||||
const handleAddLocation = (data) => {
|
||||
emit("add-new-location", data);
|
||||
};
|
||||
|
||||
const handleModifyLocation = (location) => {
|
||||
emit("modify-location", location);
|
||||
};
|
||||
|
||||
const handleRemoveLocation = (locationId) => {
|
||||
emit("remove-location", locationId);
|
||||
};
|
||||
|
||||
const getInitials = (name) => {
|
||||
if (!name) return "?";
|
||||
return name
|
||||
|
||||
@ -31,6 +31,18 @@
|
||||
<ClientAddressTab :client="client" />
|
||||
</div>
|
||||
|
||||
<!-- Locations Tab -->
|
||||
<div v-show="activeTab === 'locations'">
|
||||
<ClientLocationsTab
|
||||
:locations="locations"
|
||||
:client-id="client.id"
|
||||
:is-loading="locationIsLoading"
|
||||
@location-created="handleCreateLocation"
|
||||
@location-modified="handleModifyLocation"
|
||||
@location-removed="handleRemoveLocation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Notes Tab -->
|
||||
<div v-show="activeTab === 'notes'">
|
||||
<ClientNotesTab :notes="client.notes" />
|
||||
@ -43,6 +55,7 @@ import ClientOverview from "@/components/molecules/client/ClientOverview.vue";
|
||||
import ClientInfoTab from "@/components/molecules/client/ClientInfoTab.vue";
|
||||
import ClientContactsTab from "@/components/molecules/client/ClientContactsTab.vue";
|
||||
import ClientAddressTab from "@/components/molecules/client/ClientAddressTab.vue";
|
||||
import ClientLocationsTab from "@/components/molecules/client/ClientLocationsTab.vue";
|
||||
import ClientNotesTab from "@/components/molecules/client/ClientNotesTab.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
@ -59,6 +72,10 @@ defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
formattedAddress: {
|
||||
type: String,
|
||||
default: "Aucune adresse renseignée",
|
||||
@ -71,9 +88,21 @@ defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
locationIsLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["change-tab", "create-contact", "updatingClient"]);
|
||||
const emit = defineEmits([
|
||||
"change-tab",
|
||||
"create-contact",
|
||||
"updatingClient",
|
||||
"create-location",
|
||||
"modify-location",
|
||||
"remove-location",
|
||||
]);
|
||||
|
||||
const updateClient = (updatedClient) => {
|
||||
emit("updatingClient", updatedClient);
|
||||
};
|
||||
@ -81,4 +110,16 @@ const updateClient = (updatedClient) => {
|
||||
const handleCreateContact = (newContact) => {
|
||||
emit("create-contact", newContact);
|
||||
};
|
||||
|
||||
const handleCreateLocation = (newLocation) => {
|
||||
emit("create-location", newLocation);
|
||||
};
|
||||
|
||||
const handleModifyLocation = (location) => {
|
||||
emit("modify-location", location);
|
||||
};
|
||||
|
||||
const handleRemoveLocation = (locationId) => {
|
||||
emit("remove-location", locationId);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
<ClientTabNavigation
|
||||
:active-tab="activeTab"
|
||||
:contacts-count="contactsCount"
|
||||
:locations-count="locationsCount"
|
||||
@change-tab="$emit('change-tab', $event)"
|
||||
/>
|
||||
</div>
|
||||
@ -49,6 +50,10 @@ defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
locationsCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">Liste des localisations</h6>
|
||||
<button
|
||||
class="btn btn-primary btn-sm ms-auto"
|
||||
@click="locationModalIsVisible = true"
|
||||
>
|
||||
<i class="fas fa-plus me-1"></i>Ajouter une localisation
|
||||
</button>
|
||||
</div>
|
||||
<location-modal
|
||||
:is-visible="locationModalIsVisible"
|
||||
:client-id="clientId"
|
||||
:location-is-loading="isLoading"
|
||||
@close="locationModalIsVisible = false"
|
||||
@location-created="handleLocationCreated"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="locations.length > 0" class="table-responsive">
|
||||
<table class="table align-items-center mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||
>
|
||||
Nom
|
||||
</th>
|
||||
<th
|
||||
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||
>
|
||||
Adresse
|
||||
</th>
|
||||
<th
|
||||
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||
>
|
||||
GPS Latitude
|
||||
</th>
|
||||
<th
|
||||
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||
>
|
||||
GPS Longitude
|
||||
</th>
|
||||
<th class="text-secondary opacity-7">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="location in locations" :key="location.id">
|
||||
<td>
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div>
|
||||
<i class="fas fa-map-marker-alt text-primary me-2"></i>
|
||||
</div>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<h6 class="mb-0 text-sm">{{ location.name || "-" }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-xs font-weight-bold mb-0">
|
||||
{{ formatAddress(location) }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-xs font-weight-bold mb-0">
|
||||
{{ 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) : "-" }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-link text-secondary mb-0"
|
||||
type="button"
|
||||
:id="'dropdownMenuButton' + location.id"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i class="fa fa-ellipsis-v text-xs"></i>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu"
|
||||
:aria-labelledby="'dropdownMenuButton' + location.id"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click.prevent="handleModifyLocation(location)"
|
||||
>
|
||||
<i class="fas fa-edit me-2"></i>Modifier
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item text-danger"
|
||||
href="#"
|
||||
@click.prevent="handleRemoveLocation(location.id)"
|
||||
>
|
||||
<i class="fas fa-trash me-2"></i>Supprimer
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref } from "vue";
|
||||
import LocationModal from "../location/LocationModal.vue";
|
||||
|
||||
defineProps({
|
||||
locations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
clientId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const locationModalIsVisible = ref(false);
|
||||
|
||||
const emit = defineEmits([
|
||||
"location-created",
|
||||
"location-modified",
|
||||
"location-removed",
|
||||
]);
|
||||
|
||||
const handleLocationCreated = (newLocation) => {
|
||||
locationModalIsVisible.value = false;
|
||||
emit("location-created", newLocation);
|
||||
};
|
||||
|
||||
const handleModifyLocation = (location) => {
|
||||
emit("location-modified", location);
|
||||
};
|
||||
|
||||
const handleRemoveLocation = (locationId) => {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette localisation ?")) {
|
||||
emit("location-removed", locationId);
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddress = (location) => {
|
||||
const parts = [
|
||||
location.address_line1,
|
||||
location.address_line2,
|
||||
location.postal_code,
|
||||
location.city,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : "-";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-menu {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
@ -26,6 +26,13 @@
|
||||
:is-active="activeTab === 'address'"
|
||||
@click="$emit('change-tab', 'address')"
|
||||
/>
|
||||
<TabNavigationItem
|
||||
icon="fas fa-map-marked-alt"
|
||||
label="Localisations"
|
||||
:is-active="activeTab === 'locations'"
|
||||
:badge="locationsCount > 0 ? locationsCount : null"
|
||||
@click="$emit('change-tab', 'locations')"
|
||||
/>
|
||||
<TabNavigationItem
|
||||
icon="fas fa-sticky-note"
|
||||
label="Notes"
|
||||
@ -48,6 +55,10 @@ defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
locationsCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["change-tab"]);
|
||||
|
||||
@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="modal fade show d-block"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
style="background-color: rgba(0, 0, 0, 0.5)"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ajouter une localisation</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
@click="closeModal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Nom de la localisation*</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Ex: Siège social"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Type de localisation</label>
|
||||
<select v-model="formData.location_type" class="form-select">
|
||||
<option value="office">Bureau</option>
|
||||
<option value="warehouse">Entrepôt</option>
|
||||
<option value="store">Magasin</option>
|
||||
<option value="other">Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Adresse ligne 1*</label>
|
||||
<input
|
||||
v-model="formData.address_line1"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Numéro et nom de rue"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Adresse ligne 2</label>
|
||||
<input
|
||||
v-model="formData.address_line2"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Complément d'adresse"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Code postal*</label>
|
||||
<input
|
||||
v-model="formData.postal_code"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Ex: 75001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Ville*</label>
|
||||
<input
|
||||
v-model="formData.city"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Ex: Paris"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Pays*</label>
|
||||
<input
|
||||
v-model="formData.country_code"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Ex: FR"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Téléphone</label>
|
||||
<input
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
class="form-control"
|
||||
placeholder="+33 1 23 45 67 89"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
class="form-control"
|
||||
placeholder="contact@exemple.fr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">GPS Latitude</label>
|
||||
<input
|
||||
v-model.number="formData.gps_lat"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="-90"
|
||||
max="90"
|
||||
class="form-control"
|
||||
placeholder="Ex: 48.856614"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">GPS Longitude</label>
|
||||
<input
|
||||
v-model.number="formData.gps_lng"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="-180"
|
||||
max="180"
|
||||
class="form-control"
|
||||
placeholder="Ex: 2.352222"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
v-model="formData.is_default"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="isDefaultCheckbox"
|
||||
/>
|
||||
<label class="form-check-label" for="isDefaultCheckbox">
|
||||
Définir comme localisation par défaut
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="closeModal"
|
||||
:disabled="locationIsLoading"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="locationIsLoading"
|
||||
>
|
||||
<span
|
||||
v-if="locationIsLoading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
></span>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clientId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
locationIsLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "location-created"]);
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
location_type: "office",
|
||||
address_line1: "",
|
||||
address_line2: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
country_code: "FR",
|
||||
phone: "",
|
||||
email: "",
|
||||
gps_lat: null,
|
||||
gps_lng: null,
|
||||
is_default: false,
|
||||
client_id: props.clientId,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.clientId,
|
||||
(newVal) => {
|
||||
formData.value.client_id = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
name: "",
|
||||
location_type: "office",
|
||||
address_line1: "",
|
||||
address_line2: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
country_code: "FR",
|
||||
phone: "",
|
||||
email: "",
|
||||
gps_lat: null,
|
||||
gps_lng: null,
|
||||
is_default: false,
|
||||
client_id: props.clientId,
|
||||
};
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
resetForm();
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit("location-created", formData.value);
|
||||
resetForm();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
272
thanasoft-front/src/services/client_location.ts
Normal file
272
thanasoft-front/src/services/client_location.ts
Normal file
@ -0,0 +1,272 @@
|
||||
import { request } from "./http";
|
||||
|
||||
export interface ClientLocationAddress {
|
||||
address_line1: string | null;
|
||||
address_line2: string | null;
|
||||
postal_code: string | null;
|
||||
city: string | null;
|
||||
country_code: string;
|
||||
full_address?: string;
|
||||
}
|
||||
|
||||
export interface ClientLocation {
|
||||
id: number;
|
||||
client_id: number;
|
||||
name: string | null;
|
||||
address_line1: string | null;
|
||||
address_line2: string | null;
|
||||
postal_code: string | null;
|
||||
city: string | null;
|
||||
country_code: string;
|
||||
gps_lat: number | null;
|
||||
gps_lng: number | null;
|
||||
code_portail: string | null;
|
||||
code_alarm: string | null;
|
||||
code_funeraire: string | null;
|
||||
is_default: boolean;
|
||||
full_address?: string;
|
||||
gps_coordinates?: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
} | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ClientLocationListResponse {
|
||||
data: ClientLocation[];
|
||||
meta?: {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClientLocationResponse {
|
||||
data: ClientLocation;
|
||||
}
|
||||
|
||||
export interface CreateClientLocationPayload {
|
||||
client_id: number;
|
||||
name: string;
|
||||
address_line1?: string | null;
|
||||
address_line2?: string | null;
|
||||
postal_code?: string | null;
|
||||
city?: string | null;
|
||||
country_code?: string;
|
||||
gps_lat?: number | null;
|
||||
gps_lng?: number | null;
|
||||
code_portail?: string | null;
|
||||
code_alarm?: string | null;
|
||||
code_funeraire?: string | null;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateClientLocationPayload
|
||||
extends Partial<CreateClientLocationPayload> {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const ClientLocationService = {
|
||||
/**
|
||||
* Get all client locations with pagination
|
||||
*/
|
||||
async getAllClientLocations(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
client_id?: number;
|
||||
is_default?: boolean;
|
||||
search?: string;
|
||||
}): Promise<ClientLocationListResponse> {
|
||||
const response = await request<ClientLocationListResponse>({
|
||||
url: "/api/client-locations",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific client location by ID
|
||||
*/
|
||||
async getClientLocation(id: number): Promise<ClientLocationResponse> {
|
||||
const response = await request<ClientLocationResponse>({
|
||||
url: `/api/client-locations/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new client location
|
||||
*/
|
||||
async createClientLocation(
|
||||
payload: CreateClientLocationPayload
|
||||
): Promise<ClientLocationResponse> {
|
||||
const formattedPayload = this.transformClientLocationPayload(payload);
|
||||
|
||||
const response = await request<ClientLocationResponse>({
|
||||
url: "/api/client-locations",
|
||||
method: "post",
|
||||
data: formattedPayload,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing client location
|
||||
*/
|
||||
async updateClientLocation(
|
||||
payload: UpdateClientLocationPayload
|
||||
): Promise<ClientLocationResponse> {
|
||||
const { id, ...updateData } = payload;
|
||||
const formattedPayload = this.transformClientLocationPayload(updateData);
|
||||
|
||||
const response = await request<ClientLocationResponse>({
|
||||
url: `/api/client-locations/${id}`,
|
||||
method: "put",
|
||||
data: formattedPayload,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a client location
|
||||
*/
|
||||
async deleteClientLocation(
|
||||
id: number
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const response = await request<{ success: boolean; message: string }>({
|
||||
url: `/api/client-locations/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a location as default for a client
|
||||
*/
|
||||
async setAsDefaultLocation(id: number): Promise<ClientLocationResponse> {
|
||||
const response = await request<ClientLocationResponse>({
|
||||
url: `/api/client-locations/${id}/set-default`,
|
||||
method: "patch",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the default location for a client
|
||||
*/
|
||||
async getDefaultClientLocation(
|
||||
clientId: number
|
||||
): Promise<ClientLocationResponse | null> {
|
||||
try {
|
||||
const response = await request<ClientLocationListResponse>({
|
||||
url: "/api/client-locations",
|
||||
method: "get",
|
||||
params: {
|
||||
client_id: clientId,
|
||||
is_default: true,
|
||||
per_page: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data.length > 0 ? { data: response.data[0] } : null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching default location:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Search client locations by name, address, or city
|
||||
*/
|
||||
async getClientLocations(query: string): Promise<ClientLocation[]> {
|
||||
const response = await request<{
|
||||
data: ClientLocation[];
|
||||
}>({
|
||||
url: `/api/clients/${query}/locations`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform client location payload to match Laravel form request structure
|
||||
*/
|
||||
transformClientLocationPayload(
|
||||
payload: Partial<CreateClientLocationPayload>
|
||||
): any {
|
||||
const transformed: any = { ...payload };
|
||||
|
||||
// Ensure boolean values are properly formatted
|
||||
if (typeof transformed.is_default === "boolean") {
|
||||
transformed.is_default = transformed.is_default ? 1 : 0;
|
||||
}
|
||||
|
||||
// Set default country code if not provided
|
||||
if (!transformed.country_code) {
|
||||
transformed.country_code = "FR";
|
||||
}
|
||||
|
||||
// Remove undefined values to avoid sending them
|
||||
Object.keys(transformed).forEach((key) => {
|
||||
if (transformed[key] === undefined) {
|
||||
delete transformed[key];
|
||||
}
|
||||
});
|
||||
|
||||
return transformed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk update client locations
|
||||
*/
|
||||
async bulkUpdateLocations(
|
||||
updates: Array<{
|
||||
id: number;
|
||||
name?: string;
|
||||
is_default?: boolean;
|
||||
}>
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const response = await request<{ success: boolean; message: string }>({
|
||||
url: "/api/client-locations/bulk-update",
|
||||
method: "patch",
|
||||
data: { updates },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate address using GPS coordinates
|
||||
*/
|
||||
async validateAddress(
|
||||
locationId: number
|
||||
): Promise<{
|
||||
valid: boolean;
|
||||
coordinates: { lat: number; lng: number } | null;
|
||||
message: string;
|
||||
}> {
|
||||
const response = await request<{
|
||||
valid: boolean;
|
||||
coordinates: { lat: number; lng: number } | null;
|
||||
message: string;
|
||||
}>({
|
||||
url: `/api/client-locations/${locationId}/validate-address`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
export default ClientLocationService;
|
||||
456
thanasoft-front/src/stores/clientLocation.ts
Normal file
456
thanasoft-front/src/stores/clientLocation.ts
Normal file
@ -0,0 +1,456 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import ClientLocationService from "@/services/client_location";
|
||||
|
||||
import type {
|
||||
ClientLocation,
|
||||
CreateClientLocationPayload,
|
||||
UpdateClientLocationPayload,
|
||||
ClientLocationListResponse,
|
||||
} from "@/services/client_location";
|
||||
|
||||
export const useClientLocationStore = defineStore("clientLocation", () => {
|
||||
// State
|
||||
const clientLocations = ref<ClientLocation[]>([]);
|
||||
const currentClientLocation = ref<ClientLocation | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const searchResults = ref<ClientLocation[]>([]);
|
||||
|
||||
// Pagination state
|
||||
const pagination = ref({
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// Getters
|
||||
const allClientLocations = computed(() => clientLocations.value);
|
||||
const defaultLocations = computed(() =>
|
||||
clientLocations.value.filter((location) => location.is_default)
|
||||
);
|
||||
const nonDefaultLocations = computed(() =>
|
||||
clientLocations.value.filter((location) => !location.is_default)
|
||||
);
|
||||
const isLoading = computed(() => loading.value);
|
||||
const hasError = computed(() => error.value !== null);
|
||||
const getError = computed(() => error.value);
|
||||
const getLocationById = computed(() => (id: number) =>
|
||||
clientLocations.value.find((location) => location.id === id)
|
||||
);
|
||||
|
||||
const getDefaultLocationByClientId = computed(() => (clientId: number) =>
|
||||
clientLocations.value.find(
|
||||
(location) => location.client_id === clientId && location.is_default
|
||||
)
|
||||
);
|
||||
const getPagination = computed(() => pagination.value);
|
||||
|
||||
// Actions
|
||||
const setLoading = (isLoading: boolean) => {
|
||||
loading.value = isLoading;
|
||||
};
|
||||
|
||||
const setError = (err: string | null) => {
|
||||
error.value = err;
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const setClientLocations = (newLocations: ClientLocation[]) => {
|
||||
clientLocations.value = newLocations;
|
||||
};
|
||||
|
||||
const setCurrentClientLocation = (location: ClientLocation | null) => {
|
||||
currentClientLocation.value = location;
|
||||
};
|
||||
|
||||
const setSearchResults = (results: ClientLocation[]) => {
|
||||
searchResults.value = results;
|
||||
};
|
||||
|
||||
const setPagination = (meta: any) => {
|
||||
if (meta) {
|
||||
pagination.value = {
|
||||
current_page: meta.current_page || 1,
|
||||
last_page: meta.last_page || 1,
|
||||
per_page: meta.per_page || 10,
|
||||
total: meta.total || 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all client locations with optional pagination and filters
|
||||
*/
|
||||
const fetchClientLocations = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
client_id?: number;
|
||||
is_default?: boolean;
|
||||
search?: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.getAllClientLocations(
|
||||
params
|
||||
);
|
||||
setClientLocations(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch client locations";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch locations for a specific client
|
||||
*/
|
||||
const fetchClientLocationsByClient = async (
|
||||
clientId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
is_default?: boolean;
|
||||
}
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.getClientLocations(
|
||||
clientId,
|
||||
params
|
||||
);
|
||||
setClientLocations(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch client locations";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a single client location by ID
|
||||
*/
|
||||
const fetchClientLocation = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.getClientLocation(id);
|
||||
setCurrentClientLocation(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch client location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new client location
|
||||
*/
|
||||
const createClientLocation = async (payload: CreateClientLocationPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.createClientLocation(
|
||||
payload
|
||||
);
|
||||
const newLocation = response.data;
|
||||
|
||||
// Add the new location to the list
|
||||
clientLocations.value.push(newLocation);
|
||||
setCurrentClientLocation(newLocation);
|
||||
|
||||
// If this location is set as default, update other locations
|
||||
if (newLocation.is_default) {
|
||||
clientLocations.value = clientLocations.value.map((location) =>
|
||||
location.client_id === newLocation.client_id &&
|
||||
location.id !== newLocation.id
|
||||
? { ...location, is_default: false }
|
||||
: location
|
||||
);
|
||||
}
|
||||
|
||||
return newLocation;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to create client location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing client location
|
||||
*/
|
||||
const updateClientLocation = async (payload: UpdateClientLocationPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.updateClientLocation(
|
||||
payload
|
||||
);
|
||||
const updatedLocation = response.data;
|
||||
|
||||
// Update in the locations list
|
||||
const index = clientLocations.value.findIndex(
|
||||
(location) => location.id === updatedLocation.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
clientLocations.value[index] = updatedLocation;
|
||||
}
|
||||
|
||||
// Update current location if it's the one being edited
|
||||
if (
|
||||
currentClientLocation.value &&
|
||||
currentClientLocation.value.id === updatedLocation.id
|
||||
) {
|
||||
setCurrentClientLocation(updatedLocation);
|
||||
}
|
||||
|
||||
// If this location is set as default, update other locations
|
||||
if (updatedLocation.is_default) {
|
||||
clientLocations.value = clientLocations.value.map((location) =>
|
||||
location.client_id === updatedLocation.client_id &&
|
||||
location.id !== updatedLocation.id
|
||||
? { ...location, is_default: false }
|
||||
: location
|
||||
);
|
||||
}
|
||||
|
||||
return updatedLocation;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to update client location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a client location
|
||||
*/
|
||||
const deleteClientLocation = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.deleteClientLocation(id);
|
||||
|
||||
// Remove from the locations list
|
||||
clientLocations.value = clientLocations.value.filter(
|
||||
(location) => location.id !== id
|
||||
);
|
||||
|
||||
// Clear current location if it's the one being deleted
|
||||
if (
|
||||
currentClientLocation.value &&
|
||||
currentClientLocation.value.id === id
|
||||
) {
|
||||
setCurrentClientLocation(null);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to delete client location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a location as default for a client
|
||||
*/
|
||||
const setAsDefaultLocation = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.setAsDefaultLocation(id);
|
||||
const updatedLocation = response.data;
|
||||
|
||||
// Update all locations for this client
|
||||
clientLocations.value = clientLocations.value.map((location) =>
|
||||
location.client_id === updatedLocation.client_id
|
||||
? {
|
||||
...location,
|
||||
is_default: location.id === updatedLocation.id,
|
||||
}
|
||||
: location
|
||||
);
|
||||
|
||||
// Update current location if it's the one being set as default
|
||||
if (
|
||||
currentClientLocation.value &&
|
||||
currentClientLocation.value.id === updatedLocation.id
|
||||
) {
|
||||
setCurrentClientLocation(updatedLocation);
|
||||
}
|
||||
|
||||
return updatedLocation;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to set default location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default location for a client
|
||||
*/
|
||||
const fetchDefaultClientLocation = async (clientId: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.getDefaultClientLocation(
|
||||
clientId
|
||||
);
|
||||
return response ? response.data : null;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch default location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getClientLocations = async (clientId: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await ClientLocationService.getClientLocations(clientId);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch default location";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear current client location
|
||||
*/
|
||||
const clearCurrentClientLocation = () => {
|
||||
setCurrentClientLocation(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear search results
|
||||
*/
|
||||
const clearSearchResults = () => {
|
||||
searchResults.value = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all state
|
||||
*/
|
||||
const clearStore = () => {
|
||||
clientLocations.value = [];
|
||||
currentClientLocation.value = null;
|
||||
error.value = null;
|
||||
searchResults.value = [];
|
||||
pagination.value = {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
clientLocations,
|
||||
currentClientLocation,
|
||||
loading,
|
||||
error,
|
||||
searchResults,
|
||||
|
||||
// Getters
|
||||
allClientLocations,
|
||||
defaultLocations,
|
||||
nonDefaultLocations,
|
||||
isLoading,
|
||||
hasError,
|
||||
getError,
|
||||
getLocationById,
|
||||
getDefaultLocationByClientId,
|
||||
getPagination,
|
||||
|
||||
// Actions
|
||||
fetchClientLocations,
|
||||
fetchClientLocationsByClient,
|
||||
fetchClientLocation,
|
||||
createClientLocation,
|
||||
updateClientLocation,
|
||||
deleteClientLocation,
|
||||
setAsDefaultLocation,
|
||||
fetchDefaultClientLocation,
|
||||
clearCurrentClientLocation,
|
||||
clearSearchResults,
|
||||
clearStore,
|
||||
clearError,
|
||||
getClientLocations,
|
||||
};
|
||||
});
|
||||
@ -3,13 +3,18 @@
|
||||
v-if="clientStore.currentClient"
|
||||
:client="clientStore.currentClient"
|
||||
:contacts="contacts_client"
|
||||
:locations="locations_client"
|
||||
:is-loading="clientStore.isLoading"
|
||||
:client-avatar="clientAvatar"
|
||||
:active-tab="activeTab"
|
||||
:file-input="fileInput"
|
||||
:contact-loading="contactStore.isLoading"
|
||||
:location-loading="clientLocationStore.isLoading"
|
||||
@update-the-client="updateClient"
|
||||
@add-new-contact="createNewContact"
|
||||
@add-new-location="createNewLocation"
|
||||
@modify-location="modifyLocation"
|
||||
@remove-location="removeLocation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -18,14 +23,18 @@ import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useContactStore } from "@/stores/contactStore";
|
||||
import { useClientLocationStore } from "@/stores/clientLocation";
|
||||
import ClientDetailPresentation from "@/components/Organism/CRM/ClientDetailPresentation.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const clientStore = useClientStore();
|
||||
const contactStore = useContactStore();
|
||||
const clientLocationStore = useClientLocationStore();
|
||||
|
||||
// Ensure client_id is a number
|
||||
const client_id = Number(route.params.id);
|
||||
const contacts_client = ref([]);
|
||||
const locations_client = ref([]);
|
||||
const activeTab = ref("overview");
|
||||
const clientAvatar = ref(null);
|
||||
const fileInput = ref(null);
|
||||
@ -34,6 +43,10 @@ onMounted(async () => {
|
||||
if (client_id) {
|
||||
await clientStore.fetchClient(client_id);
|
||||
contacts_client.value = await contactStore.getClientListContact(client_id);
|
||||
const locationsResponse = await clientLocationStore.getClientLocations(
|
||||
client_id
|
||||
);
|
||||
locations_client.value = locationsResponse || [];
|
||||
}
|
||||
});
|
||||
|
||||
@ -61,4 +74,37 @@ const createNewContact = async (data) => {
|
||||
console.error("Error creating contact:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewLocation = async (data) => {
|
||||
try {
|
||||
await clientLocationStore.createClientLocation(data);
|
||||
// Refresh locations list after creation
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
} catch (error) {
|
||||
console.error("Error creating location:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const modifyLocation = async (location) => {
|
||||
try {
|
||||
await clientLocationStore.updateClientLocation(location);
|
||||
// Refresh locations list after modification
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
} catch (error) {
|
||||
console.error("Error modifying location:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeLocation = async (locationId) => {
|
||||
try {
|
||||
await clientLocationStore.deleteClientLocation(locationId);
|
||||
// Refresh locations list after deletion
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
} catch (error) {
|
||||
console.error("Error removing location:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user