add location

This commit is contained in:
Nyavokevin 2025-10-21 12:38:07 +03:00
parent 78700a3c5a
commit b62cb3d717
9 changed files with 1328 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]);

View File

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

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

View 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,
};
});

View File

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