Compare commits
2 Commits
e2cb4499bb
...
b62cb3d717
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b62cb3d717 | ||
|
|
78700a3c5a |
@ -63,6 +63,28 @@ class ClientLocationController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified client id.
|
||||||
|
*/
|
||||||
|
public function getLocationsByClient(string $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$clientLocations = $this->clientLocationRepository->getByClientId((int)$id);
|
||||||
|
return ClientLocationResource::collection($clientLocations);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching client location: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'client_location_id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération du lieu client.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the specified client location.
|
* Display the specified client location.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -12,4 +12,11 @@ class ClientLocationRepository extends BaseRepository implements ClientLocationR
|
|||||||
{
|
{
|
||||||
parent::__construct($model);
|
parent::__construct($model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getByClientId(int $client_id)
|
||||||
|
{
|
||||||
|
$query = $this->model->newQuery();
|
||||||
|
$query->where('client_id', $client_id);
|
||||||
|
return $query->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,5 +6,5 @@ namespace App\Repositories;
|
|||||||
|
|
||||||
interface ClientLocationRepositoryInterface extends BaseRepositoryInterface
|
interface ClientLocationRepositoryInterface extends BaseRepositoryInterface
|
||||||
{
|
{
|
||||||
// Add ClientLocation-specific methods here later if needed
|
function getByClientId(int $client_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,8 +39,9 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
|
|
||||||
Route::apiResource('clients', ClientController::class);
|
Route::apiResource('clients', ClientController::class);
|
||||||
Route::apiResource('client-groups', ClientGroupController::class);
|
Route::apiResource('client-groups', ClientGroupController::class);
|
||||||
Route::apiResource('client-locations', ClientLocationController::class);
|
|
||||||
|
|
||||||
|
Route::apiResource('client-locations', ClientLocationController::class);
|
||||||
|
Route::get('clients/{clientId}/locations', [ClientLocationController::class, 'getLocationsByClient']);
|
||||||
|
|
||||||
// Contact management
|
// Contact management
|
||||||
Route::apiResource('contacts', ContactController::class);
|
Route::apiResource('contacts', ContactController::class);
|
||||||
|
|||||||
@ -23,7 +23,8 @@
|
|||||||
:initials="getInitials(client.name)"
|
:initials="getInitials(client.name)"
|
||||||
:client-name="client.name"
|
:client-name="client.name"
|
||||||
:client-type="client.type_label || 'Client'"
|
:client-type="client.type_label || 'Client'"
|
||||||
:contacts-count="client.length"
|
:contacts-count="contacts.length"
|
||||||
|
:locations-count="locations.length"
|
||||||
:is-active="client.is_active"
|
:is-active="client.is_active"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
@edit-avatar="triggerFileInput"
|
@edit-avatar="triggerFileInput"
|
||||||
@ -44,12 +45,17 @@
|
|||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:client="client"
|
:client="client"
|
||||||
:contacts="contacts"
|
:contacts="contacts"
|
||||||
|
:locations="locations"
|
||||||
:formatted-address="formatAddress(client)"
|
:formatted-address="formatAddress(client)"
|
||||||
:client-id="client.id"
|
:client-id="client.id"
|
||||||
:contact-is-loading="contactLoading"
|
:contact-is-loading="contactLoading"
|
||||||
|
:location-is-loading="locationLoading"
|
||||||
@change-tab="activeTab = $event"
|
@change-tab="activeTab = $event"
|
||||||
@updating-client="handleUpdateClient"
|
@updating-client="handleUpdateClient"
|
||||||
@create-contact="handleAddContact"
|
@create-contact="handleAddContact"
|
||||||
|
@create-location="handleAddLocation"
|
||||||
|
@modify-location="handleModifyLocation"
|
||||||
|
@remove-location="handleRemoveLocation"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</client-detail-template>
|
</client-detail-template>
|
||||||
@ -71,6 +77,11 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
locations: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -91,6 +102,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
locationLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const localAvatar = ref(props.clientAvatar);
|
const localAvatar = ref(props.clientAvatar);
|
||||||
@ -99,6 +114,9 @@ const emit = defineEmits([
|
|||||||
"updateTheClient",
|
"updateTheClient",
|
||||||
"handleFileInput",
|
"handleFileInput",
|
||||||
"add-new-contact",
|
"add-new-contact",
|
||||||
|
"add-new-location",
|
||||||
|
"modify-location",
|
||||||
|
"remove-location",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleAvatarUpload = (event) => {
|
const handleAvatarUpload = (event) => {
|
||||||
@ -123,10 +141,21 @@ const inputFile = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddContact = (data) => {
|
const handleAddContact = (data) => {
|
||||||
// TODO: Implement add contact functionality
|
|
||||||
emit("add-new-contact", data);
|
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) => {
|
const getInitials = (name) => {
|
||||||
if (!name) return "?";
|
if (!name) return "?";
|
||||||
return name
|
return name
|
||||||
|
|||||||
@ -31,6 +31,18 @@
|
|||||||
<ClientAddressTab :client="client" />
|
<ClientAddressTab :client="client" />
|
||||||
</div>
|
</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 -->
|
<!-- Notes Tab -->
|
||||||
<div v-show="activeTab === 'notes'">
|
<div v-show="activeTab === 'notes'">
|
||||||
<ClientNotesTab :notes="client.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 ClientInfoTab from "@/components/molecules/client/ClientInfoTab.vue";
|
||||||
import ClientContactsTab from "@/components/molecules/client/ClientContactsTab.vue";
|
import ClientContactsTab from "@/components/molecules/client/ClientContactsTab.vue";
|
||||||
import ClientAddressTab from "@/components/molecules/client/ClientAddressTab.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 ClientNotesTab from "@/components/molecules/client/ClientNotesTab.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
@ -59,6 +72,10 @@ defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
locations: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
formattedAddress: {
|
formattedAddress: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Aucune adresse renseignée",
|
default: "Aucune adresse renseignée",
|
||||||
@ -71,9 +88,21 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
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) => {
|
const updateClient = (updatedClient) => {
|
||||||
emit("updatingClient", updatedClient);
|
emit("updatingClient", updatedClient);
|
||||||
};
|
};
|
||||||
@ -81,4 +110,16 @@ const updateClient = (updatedClient) => {
|
|||||||
const handleCreateContact = (newContact) => {
|
const handleCreateContact = (newContact) => {
|
||||||
emit("create-contact", 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>
|
</script>
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
<ClientTabNavigation
|
<ClientTabNavigation
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:contacts-count="contactsCount"
|
:contacts-count="contactsCount"
|
||||||
|
:locations-count="locationsCount"
|
||||||
@change-tab="$emit('change-tab', $event)"
|
@change-tab="$emit('change-tab', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -49,6 +50,10 @@ defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
locationsCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
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'"
|
:is-active="activeTab === 'address'"
|
||||||
@click="$emit('change-tab', '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
|
<TabNavigationItem
|
||||||
icon="fas fa-sticky-note"
|
icon="fas fa-sticky-note"
|
||||||
label="Notes"
|
label="Notes"
|
||||||
@ -48,6 +55,10 @@ defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
locationsCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["change-tab"]);
|
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"
|
v-if="clientStore.currentClient"
|
||||||
:client="clientStore.currentClient"
|
:client="clientStore.currentClient"
|
||||||
:contacts="contacts_client"
|
:contacts="contacts_client"
|
||||||
|
:locations="locations_client"
|
||||||
:is-loading="clientStore.isLoading"
|
:is-loading="clientStore.isLoading"
|
||||||
:client-avatar="clientAvatar"
|
:client-avatar="clientAvatar"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:file-input="fileInput"
|
:file-input="fileInput"
|
||||||
:contact-loading="contactStore.isLoading"
|
:contact-loading="contactStore.isLoading"
|
||||||
|
:location-loading="clientLocationStore.isLoading"
|
||||||
@update-the-client="updateClient"
|
@update-the-client="updateClient"
|
||||||
@add-new-contact="createNewContact"
|
@add-new-contact="createNewContact"
|
||||||
|
@add-new-location="createNewLocation"
|
||||||
|
@modify-location="modifyLocation"
|
||||||
|
@remove-location="removeLocation"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -18,14 +23,18 @@ import { ref, onMounted } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
import { useContactStore } from "@/stores/contactStore";
|
import { useContactStore } from "@/stores/contactStore";
|
||||||
|
import { useClientLocationStore } from "@/stores/clientLocation";
|
||||||
import ClientDetailPresentation from "@/components/Organism/CRM/ClientDetailPresentation.vue";
|
import ClientDetailPresentation from "@/components/Organism/CRM/ClientDetailPresentation.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
const contactStore = useContactStore();
|
const contactStore = useContactStore();
|
||||||
|
const clientLocationStore = useClientLocationStore();
|
||||||
|
|
||||||
// Ensure client_id is a number
|
// Ensure client_id is a number
|
||||||
const client_id = Number(route.params.id);
|
const client_id = Number(route.params.id);
|
||||||
const contacts_client = ref([]);
|
const contacts_client = ref([]);
|
||||||
|
const locations_client = ref([]);
|
||||||
const activeTab = ref("overview");
|
const activeTab = ref("overview");
|
||||||
const clientAvatar = ref(null);
|
const clientAvatar = ref(null);
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
@ -34,6 +43,10 @@ onMounted(async () => {
|
|||||||
if (client_id) {
|
if (client_id) {
|
||||||
await clientStore.fetchClient(client_id);
|
await clientStore.fetchClient(client_id);
|
||||||
contacts_client.value = await contactStore.getClientListContact(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);
|
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>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user