add notification et crud fournisseur
This commit is contained in:
parent
ca09f6da2f
commit
4b056038d6
@ -187,4 +187,28 @@ class ContactController extends Controller
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function getContactsByFournisseur(string $fournisseurId): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$intId = (int) $fournisseurId;
|
||||||
|
$contacts = $this->contactRepository->getByFournisseurId($intId);
|
||||||
|
return response()->json([
|
||||||
|
'data' => ContactResource::collection($contacts),
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching contacts by fournisseur: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'fournisseur_id' => $fournisseurId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des contacts du fournisseur.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@ class StoreContactRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id' => 'required|exists:clients,id',
|
'client_id' => 'nullable|exists:clients,id',
|
||||||
|
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
||||||
'first_name' => 'nullable|string|max:191',
|
'first_name' => 'nullable|string|max:191',
|
||||||
'last_name' => 'nullable|string|max:191',
|
'last_name' => 'nullable|string|max:191',
|
||||||
'email' => 'nullable|email|max:191',
|
'email' => 'nullable|email|max:191',
|
||||||
@ -34,8 +35,8 @@ class StoreContactRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.required' => 'Le client est obligatoire.',
|
|
||||||
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
|
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
|
||||||
|
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
||||||
'first_name.string' => 'Le prénom doit être une chaîne de caractères.',
|
'first_name.string' => 'Le prénom doit être une chaîne de caractères.',
|
||||||
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
|
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
|
||||||
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
|
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
|
||||||
@ -50,9 +51,17 @@ class StoreContactRequest extends FormRequest
|
|||||||
public function withValidator($validator)
|
public function withValidator($validator)
|
||||||
{
|
{
|
||||||
$validator->after(function ($validator) {
|
$validator->after(function ($validator) {
|
||||||
|
// At least one of client_id or fournisseur_id must be provided
|
||||||
|
if (empty($this->client_id) && empty($this->fournisseur_id)) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'general',
|
||||||
|
'Le contact doit être associé à un client ou un fournisseur.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
|
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
|
||||||
$validator->errors()->add(
|
$validator->errors()->add(
|
||||||
'general',
|
'general',
|
||||||
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
|
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@ class UpdateContactRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id' => 'required|exists:clients,id',
|
'client_id' => 'nullable|exists:clients,id',
|
||||||
|
'fournisseur_id' => 'nullable|exists:fournisseurs,id',
|
||||||
'first_name' => 'nullable|string|max:191',
|
'first_name' => 'nullable|string|max:191',
|
||||||
'last_name' => 'nullable|string|max:191',
|
'last_name' => 'nullable|string|max:191',
|
||||||
'email' => 'nullable|email|max:191',
|
'email' => 'nullable|email|max:191',
|
||||||
@ -34,8 +35,8 @@ class UpdateContactRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'client_id.required' => 'Le client est obligatoire.',
|
|
||||||
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
|
'client_id.exists' => 'Le client sélectionné n\'existe pas.',
|
||||||
|
'fournisseur_id.exists' => 'Le fournisseur sélectionné n\'existe pas.',
|
||||||
'first_name.string' => 'Le prénom doit être une chaîne de caractères.',
|
'first_name.string' => 'Le prénom doit être une chaîne de caractères.',
|
||||||
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
|
'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.',
|
||||||
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
|
'last_name.string' => 'Le nom doit être une chaîne de caractères.',
|
||||||
@ -51,9 +52,17 @@ class UpdateContactRequest extends FormRequest
|
|||||||
public function withValidator($validator)
|
public function withValidator($validator)
|
||||||
{
|
{
|
||||||
$validator->after(function ($validator) {
|
$validator->after(function ($validator) {
|
||||||
|
// At least one of client_id or fournisseur_id must be provided
|
||||||
|
if (empty($this->client_id) && empty($this->fournisseur_id)) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'general',
|
||||||
|
'Le contact doit être associé à un client ou un fournisseur.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
|
if (empty($this->first_name) && empty($this->last_name) && empty($this->email) && empty($this->phone)) {
|
||||||
$validator->errors()->add(
|
$validator->errors()->add(
|
||||||
'general',
|
'general',
|
||||||
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
|
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class ContactResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'client_id' => $this->client_id,
|
'client_id' => $this->client_id,
|
||||||
|
'fournisseur_id' => $this->fournisseur_id,
|
||||||
'first_name' => $this->first_name,
|
'first_name' => $this->first_name,
|
||||||
'last_name' => $this->last_name,
|
'last_name' => $this->last_name,
|
||||||
'full_name' => $this->full_name,
|
'full_name' => $this->full_name,
|
||||||
@ -28,10 +29,18 @@ class ContactResource extends JsonResource
|
|||||||
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
|
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
'client' => $this->whenLoaded('client', [
|
'client' => $this->whenLoaded('client', function() {
|
||||||
'id' => $this->client->id,
|
return $this->client ? [
|
||||||
'name' => $this->client->name,
|
'id' => $this->client->id,
|
||||||
]),
|
'name' => $this->client->name,
|
||||||
|
] : null;
|
||||||
|
}),
|
||||||
|
'fournisseur' => $this->whenLoaded('fournisseur', function() {
|
||||||
|
return $this->fournisseur ? [
|
||||||
|
'id' => $this->fournisseur->id,
|
||||||
|
'name' => $this->fournisseur->name,
|
||||||
|
] : null;
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,8 @@ class Contact extends Model
|
|||||||
'position',
|
'position',
|
||||||
'notes',
|
'notes',
|
||||||
'is_primary',
|
'is_primary',
|
||||||
'client_id'
|
'client_id',
|
||||||
|
'fournisseur_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -28,6 +29,11 @@ class Contact extends Model
|
|||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function fournisseur(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Fournisseur::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the contact's full name.
|
* Get the contact's full name.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -62,4 +62,11 @@ class ContactRepository extends BaseRepository implements ContactRepositoryInter
|
|||||||
->where('client_id', $clientId)
|
->where('client_id', $clientId)
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getByFournisseurId(int $fournisseurId)
|
||||||
|
{
|
||||||
|
return $this->model->newQuery()
|
||||||
|
->where('fournisseur_id', $fournisseurId)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,4 +9,6 @@ interface ContactRepositoryInterface extends BaseRepositoryInterface
|
|||||||
function paginate(int $perPage = 15, array $filters = []);
|
function paginate(int $perPage = 15, array $filters = []);
|
||||||
|
|
||||||
function getByClientId(int $clientId);
|
function getByClientId(int $clientId);
|
||||||
|
|
||||||
|
function getByFournisseurId(int $fournisseurId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('contacts', function (Blueprint $table) {
|
||||||
|
// Make client_id nullable and remove cascade
|
||||||
|
$table->dropForeign(['client_id']);
|
||||||
|
$table->foreignId('client_id')->nullable()->change();
|
||||||
|
$table->foreign('client_id')->references('id')->on('clients')->onDelete('set null');
|
||||||
|
|
||||||
|
// Add fournisseur_id
|
||||||
|
$table->foreignId('fournisseur_id')->nullable()->after('client_id')->constrained('fournisseurs')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('contacts', function (Blueprint $table) {
|
||||||
|
// Remove fournisseur_id
|
||||||
|
$table->dropForeign(['fournisseur_id']);
|
||||||
|
$table->dropColumn('fournisseur_id');
|
||||||
|
|
||||||
|
// Restore client_id to not nullable with cascade
|
||||||
|
$table->dropForeign(['client_id']);
|
||||||
|
$table->foreignId('client_id')->change();
|
||||||
|
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -53,4 +53,5 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
// Fournisseur management
|
// Fournisseur management
|
||||||
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
||||||
Route::apiResource('fournisseurs', FournisseurController::class);
|
Route::apiResource('fournisseurs', FournisseurController::class);
|
||||||
|
Route::get('fournisseurs/{fournisseurId}/contacts', [ContactController::class, 'getContactsByFournisseur']);
|
||||||
});
|
});
|
||||||
|
|||||||
252
thanasoft-front/src/components/GlobalNotification.vue
Normal file
252
thanasoft-front/src/components/GlobalNotification.vue
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<div class="notification-container">
|
||||||
|
<transition-group name="notification" tag="div">
|
||||||
|
<div
|
||||||
|
v-for="notification in notificationStore.notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
:class="['notification-item', `notification-${notification.type}`]"
|
||||||
|
@click="notificationStore.removeNotification(notification.id)"
|
||||||
|
>
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-icon">
|
||||||
|
<i :class="getIcon(notification.type)"></i>
|
||||||
|
</div>
|
||||||
|
<div class="notification-text">
|
||||||
|
<h6 class="notification-title mb-1">{{ notification.title }}</h6>
|
||||||
|
<p class="notification-message mb-0">
|
||||||
|
{{ notification.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="notification-close"
|
||||||
|
@click.stop="
|
||||||
|
notificationStore.removeNotification(notification.id)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="notification-progress">
|
||||||
|
<div
|
||||||
|
class="notification-progress-bar"
|
||||||
|
:style="{ animationDuration: `${notification.duration}ms` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const getIcon = (type) => {
|
||||||
|
const icons = {
|
||||||
|
success: "fas fa-check-circle",
|
||||||
|
error: "fas fa-exclamation-circle",
|
||||||
|
warning: "fas fa-exclamation-triangle",
|
||||||
|
info: "fas fa-info-circle",
|
||||||
|
};
|
||||||
|
return icons[type] || icons.info;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 400px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
pointer-events: all;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item:hover {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-success {
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning {
|
||||||
|
border-left-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info {
|
||||||
|
border-left-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-success .notification-icon {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error .notification-icon {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning .notification-icon {
|
||||||
|
background-color: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info .notification-icon {
|
||||||
|
background-color: rgba(23, 162, 184, 0.1);
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344767;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #67748e;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #67748e;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-close:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-progress {
|
||||||
|
height: 3px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: currentColor;
|
||||||
|
animation: progress linear forwards;
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-success .notification-progress-bar {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error .notification-progress-bar {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-warning .notification-progress-bar {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-info .notification-progress-bar {
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress {
|
||||||
|
from {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition animations */
|
||||||
|
.notification-enter-active,
|
||||||
|
.notification-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%) scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-move {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notification-container {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<new-fournisseur-template>
|
||||||
|
<template #multi-step> </template>
|
||||||
|
<template #fournisseur-form>
|
||||||
|
<new-fournisseur-form
|
||||||
|
:loading="loading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="success"
|
||||||
|
@create-fournisseur="handleCreateFournisseur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</new-fournisseur-template>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import NewFournisseurTemplate from "@/components/templates/CRM/NewFournisseurTemplate.vue";
|
||||||
|
import NewFournisseurForm from "@/components/molecules/form/NewFournisseurForm.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["createFournisseur"]);
|
||||||
|
|
||||||
|
const handleCreateFournisseur = (data) => {
|
||||||
|
emit("createFournisseur", data);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
:initials="getInitials(fournisseur.name)"
|
:initials="getInitials(fournisseur.name)"
|
||||||
:fournisseur-name="fournisseur.name"
|
:fournisseur-name="fournisseur.name"
|
||||||
:fournisseur-type="fournisseur.type_label || 'Fournisseur'"
|
:fournisseur-type="fournisseur.type_label || 'Fournisseur'"
|
||||||
:contacts-count="contacts.length"
|
:contacts-count="filteredContactsCount"
|
||||||
:locations-count="locations.length"
|
:locations-count="locations.length"
|
||||||
:is-active="fournisseur.is_active"
|
:is-active="fournisseur.is_active"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
@ -41,10 +41,9 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #fournisseur-detail-content>
|
<template #fournisseur-detail-content>
|
||||||
<FournisseurDetailContent
|
<fournisseur-detail-content
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:fournisseur="fournisseur"
|
:fournisseur="fournisseur"
|
||||||
:contacts="contacts"
|
|
||||||
:locations="locations"
|
:locations="locations"
|
||||||
:formatted-address="formatAddress(fournisseur)"
|
:formatted-address="formatAddress(fournisseur)"
|
||||||
:fournisseur-id="fournisseur.id"
|
:fournisseur-id="fournisseur.id"
|
||||||
@ -54,6 +53,7 @@
|
|||||||
@updating-fournisseur="handleUpdateFournisseur"
|
@updating-fournisseur="handleUpdateFournisseur"
|
||||||
@create-contact="handleAddContact"
|
@create-contact="handleAddContact"
|
||||||
@updating-contact="handleModifiedContact"
|
@updating-contact="handleModifiedContact"
|
||||||
|
@contact-removed="handleRemovedContact"
|
||||||
@create-location="handleAddLocation"
|
@create-location="handleAddLocation"
|
||||||
@modify-location="handleModifyLocation"
|
@modify-location="handleModifyLocation"
|
||||||
@remove-location="handleRemoveLocation"
|
@remove-location="handleRemoveLocation"
|
||||||
@ -62,7 +62,8 @@
|
|||||||
</fournisseur-detail-template>
|
</fournisseur-detail-template>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref } from "vue";
|
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||||
|
import { useContactStore } from "@/stores/contactStore";
|
||||||
import FournisseurDetailTemplate from "@/components/templates/CRM/FournisseurDetailTemplate.vue";
|
import FournisseurDetailTemplate from "@/components/templates/CRM/FournisseurDetailTemplate.vue";
|
||||||
import FournisseurDetailSidebar from "./fournisseur/FournisseurDetailSidebar.vue";
|
import FournisseurDetailSidebar from "./fournisseur/FournisseurDetailSidebar.vue";
|
||||||
import FournisseurDetailContent from "./fournisseur/FournisseurDetailContent.vue";
|
import FournisseurDetailContent from "./fournisseur/FournisseurDetailContent.vue";
|
||||||
@ -73,11 +74,6 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
contacts: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
locations: {
|
locations: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@ -87,6 +83,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
contactIsLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
fournisseurAvatar: {
|
fournisseurAvatar: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
@ -109,6 +109,15 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use contact store to get filtered contacts count
|
||||||
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
|
const filteredContactsCount = computed(() => {
|
||||||
|
return contactStore.contacts.filter(
|
||||||
|
(contact) => contact.fournisseur_id === props.fournisseur.id
|
||||||
|
).length;
|
||||||
|
});
|
||||||
|
|
||||||
const localAvatar = ref(props.fournisseurAvatar);
|
const localAvatar = ref(props.fournisseurAvatar);
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@ -116,6 +125,7 @@ const emit = defineEmits([
|
|||||||
"handleFileInput",
|
"handleFileInput",
|
||||||
"add-new-contact",
|
"add-new-contact",
|
||||||
"updating-contact",
|
"updating-contact",
|
||||||
|
"contact-removed",
|
||||||
"add-new-location",
|
"add-new-location",
|
||||||
"modify-location",
|
"modify-location",
|
||||||
"remove-location",
|
"remove-location",
|
||||||
@ -150,6 +160,10 @@ const handleModifiedContact = (modifiedContact) => {
|
|||||||
emit("updating-contact", modifiedContact);
|
emit("updating-contact", modifiedContact);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemovedContact = (contactId) => {
|
||||||
|
emit("contact-removed", contactId);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddLocation = (data) => {
|
const handleAddLocation = (data) => {
|
||||||
emit("add-new-location", data);
|
emit("add-new-location", data);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -44,8 +44,9 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const goToFournisseur = () => {
|
const goToFournisseur = () => {
|
||||||
// Navigate to create fournisseur page when implemented
|
router.push({
|
||||||
console.log("Navigate to create fournisseur");
|
name: "Creation fournisseur",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToDetails = (fournisseurId) => {
|
const goToDetails = (fournisseurId) => {
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
<div v-show="activeTab === 'overview'">
|
<div v-show="activeTab === 'overview'">
|
||||||
<FournisseurOverview
|
<FournisseurOverview
|
||||||
:fournisseur="fournisseur"
|
:fournisseur="fournisseur"
|
||||||
:contacts="contacts"
|
|
||||||
:formatted-address="formattedAddress"
|
:formatted-address="formattedAddress"
|
||||||
:fournisseur-id="fournisseurId"
|
:fournisseur-id="fournisseurId"
|
||||||
@view-all-contacts="$emit('change-tab', 'contacts')"
|
@view-all-contacts="$emit('change-tab', 'contacts')"
|
||||||
@ -26,11 +25,11 @@
|
|||||||
<!-- Contacts Tab -->
|
<!-- Contacts Tab -->
|
||||||
<div v-show="activeTab === 'contacts'">
|
<div v-show="activeTab === 'contacts'">
|
||||||
<FournisseurContactsTab
|
<FournisseurContactsTab
|
||||||
:contacts="contacts"
|
|
||||||
:fournisseur-id="fournisseur.id"
|
:fournisseur-id="fournisseur.id"
|
||||||
:is-loading="contactIsLoading"
|
:is-loading="contactIsLoading"
|
||||||
@contact-created="handleCreateContact"
|
@contact-created="handleCreateContact"
|
||||||
@contact-modified="handleModifiedContact"
|
@contact-modified="handleModifiedContact"
|
||||||
|
@contact-removed="handleRemovedContact"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -77,10 +76,6 @@ defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
contacts: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
locations: {
|
locations: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@ -111,6 +106,7 @@ const emit = defineEmits([
|
|||||||
"modify-location",
|
"modify-location",
|
||||||
"remove-location",
|
"remove-location",
|
||||||
"updating-contact",
|
"updating-contact",
|
||||||
|
"contact-removed",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateFournisseur = (updatedFournisseur) => {
|
const updateFournisseur = (updatedFournisseur) => {
|
||||||
@ -136,4 +132,10 @@ const handleModifyLocation = (location) => {
|
|||||||
const handleRemoveLocation = (locationId) => {
|
const handleRemoveLocation = (locationId) => {
|
||||||
emit("remove-location", locationId);
|
emit("remove-location", locationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemovedContact = (contactId) => {
|
||||||
|
// The contact is already removed from the store,
|
||||||
|
// so we just need to notify parent components if needed
|
||||||
|
emit("contact-removed", contactId);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,19 +3,6 @@
|
|||||||
<h5 class="font-weight-bolder mb-0">Nouveau Client</h5>
|
<h5 class="font-weight-bolder mb-0">Nouveau Client</h5>
|
||||||
<p class="mb-0 text-sm">Informations du client</p>
|
<p class="mb-0 text-sm">Informations du client</p>
|
||||||
|
|
||||||
<!-- Message de succès -->
|
|
||||||
<div
|
|
||||||
v-if="props.success"
|
|
||||||
class="alert alert-success alert-dismissible fade show mt-3"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<span class="alert-icon"><i class="ni ni-like-2"></i></span>
|
|
||||||
<span class="alert-text"
|
|
||||||
><strong>Succès !</strong> Client créé avec succès ! Redirection en
|
|
||||||
cours...</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="multisteps-form__content">
|
<div class="multisteps-form__content">
|
||||||
<!-- Catégorie du client -->
|
<!-- Catégorie du client -->
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
|||||||
@ -0,0 +1,486 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||||
|
<h5 class="font-weight-bolder mb-0">Nouveau Fournisseur</h5>
|
||||||
|
<p class="mb-0 text-sm">Informations du fournisseur</p>
|
||||||
|
|
||||||
|
<div class="multisteps-form__content">
|
||||||
|
<!-- Nom du fournisseur -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label"
|
||||||
|
>Nom du fournisseur <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<soft-input
|
||||||
|
:value="form.name"
|
||||||
|
@input="form.name = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.name }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Nom de l'entreprise"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.name" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TVA & SIRET -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Numéro de TVA</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.vat_number"
|
||||||
|
@input="form.vat_number = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.vat_number }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. FR12345678901"
|
||||||
|
maxlength="32"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.vat_number" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.vat_number }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">SIRET</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.siret"
|
||||||
|
@input="form.siret = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.siret }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. 12345678901234"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.siret" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.siret }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations de contact -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.email"
|
||||||
|
@input="form.email = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.email }"
|
||||||
|
type="email"
|
||||||
|
placeholder="ex. contact@fournisseur.com"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.email" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Téléphone</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.phone"
|
||||||
|
@input="form.phone = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.phone }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. +33 1 23 45 67 89"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.phone" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.phone }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Adresse de facturation -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Adresse ligne 1</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.billing_address_line1"
|
||||||
|
@input="form.billing_address_line1 = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.billing_address_line1 }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. 123 Rue Principale"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.billing_address_line1"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.billing_address_line1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Adresse ligne 2</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.billing_address_line2"
|
||||||
|
@input="form.billing_address_line2 = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.billing_address_line2 }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Bâtiment, Étage, etc."
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="fieldErrors.billing_address_line2"
|
||||||
|
class="invalid-feedback"
|
||||||
|
>
|
||||||
|
{{ fieldErrors.billing_address_line2 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-4">
|
||||||
|
<label class="form-label">Code postal</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.billing_postal_code"
|
||||||
|
@input="form.billing_postal_code = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.billing_postal_code }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. 75001"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.billing_postal_code" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.billing_postal_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-4 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Ville</label>
|
||||||
|
<soft-input
|
||||||
|
:value="form.billing_city"
|
||||||
|
@input="form.billing_city = $event.target.value"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.billing_city }"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Paris"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.billing_city" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.billing_city }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-4 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Code pays</label>
|
||||||
|
<select
|
||||||
|
:value="form.billing_country_code"
|
||||||
|
@input="form.billing_country_code = $event.target.value"
|
||||||
|
class="form-control multisteps-form__input"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.billing_country_code }"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un pays</option>
|
||||||
|
<option value="FR">France</option>
|
||||||
|
<option value="BE">Belgique</option>
|
||||||
|
<option value="CH">Suisse</option>
|
||||||
|
<option value="LU">Luxembourg</option>
|
||||||
|
<option value="DE">Allemagne</option>
|
||||||
|
<option value="ES">Espagne</option>
|
||||||
|
<option value="IT">Italie</option>
|
||||||
|
<option value="GB">Royaume-Uni</option>
|
||||||
|
<option value="MG">Madagascar</option>
|
||||||
|
</select>
|
||||||
|
<div v-if="fieldErrors.billing_country_code" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.billing_country_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<textarea
|
||||||
|
:value="form.notes"
|
||||||
|
@input="form.notes = $event.target.value"
|
||||||
|
class="form-control multisteps-form__input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Notes supplémentaires sur le fournisseur..."
|
||||||
|
maxlength="1000"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statut -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="isActive"
|
||||||
|
:checked="form.is_active"
|
||||||
|
@change="form.is_active = $event.target.checked"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="isActive">
|
||||||
|
Fournisseur actif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons -->
|
||||||
|
<div class="button-row d-flex mt-4">
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="me-2 mb-0"
|
||||||
|
@click="resetForm"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="dark"
|
||||||
|
variant="gradient"
|
||||||
|
class="ms-auto mb-0"
|
||||||
|
:disabled="props.loading"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="props.loading"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
{{ props.loading ? "Création..." : "Créer le fournisseur" }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["createFournisseur"]);
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const errors = ref([]);
|
||||||
|
const fieldErrors = ref({});
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
vat_number: "",
|
||||||
|
siret: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
billing_address_line1: "",
|
||||||
|
billing_address_line2: "",
|
||||||
|
billing_postal_code: "",
|
||||||
|
billing_city: "",
|
||||||
|
billing_country_code: "",
|
||||||
|
notes: "",
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for validation errors from parent
|
||||||
|
watch(
|
||||||
|
() => props.validationErrors,
|
||||||
|
(newErrors) => {
|
||||||
|
fieldErrors.value = { ...newErrors };
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch for success from parent
|
||||||
|
watch(
|
||||||
|
() => props.success,
|
||||||
|
(newSuccess) => {
|
||||||
|
if (newSuccess) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
// Clear errors before submitting
|
||||||
|
fieldErrors.value = {};
|
||||||
|
errors.value = [];
|
||||||
|
|
||||||
|
// Client-side validation
|
||||||
|
const validationResult = validateForm();
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
fieldErrors.value = validationResult.errors;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up form data: convert empty strings to null
|
||||||
|
const cleanedForm = {};
|
||||||
|
const formData = form.value;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (value === "" || value === null || value === undefined) {
|
||||||
|
cleanedForm[key] = null;
|
||||||
|
} else {
|
||||||
|
cleanedForm[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure is_active is boolean
|
||||||
|
cleanedForm.is_active = Boolean(formData.is_active);
|
||||||
|
|
||||||
|
console.log("Form data being emitted:", cleanedForm);
|
||||||
|
|
||||||
|
// Emit the cleaned form data to parent
|
||||||
|
emit("createFournisseur", cleanedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const errors = {};
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Nom requis
|
||||||
|
if (!form.value.name || form.value.name.trim() === "") {
|
||||||
|
errors.name = "Le nom du fournisseur est obligatoire";
|
||||||
|
isValid = false;
|
||||||
|
} else if (form.value.name.length > 255) {
|
||||||
|
errors.name = "Le nom ne peut pas dépasser 255 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (form.value.email && form.value.email.trim() !== "") {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(form.value.email)) {
|
||||||
|
errors.email = "L'adresse email doit être valide";
|
||||||
|
isValid = false;
|
||||||
|
} else if (form.value.email.length > 191) {
|
||||||
|
errors.email = "L'email ne peut pas dépasser 191 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VAT number validation
|
||||||
|
if (form.value.vat_number && form.value.vat_number.length > 32) {
|
||||||
|
errors.vat_number = "Le numéro de TVA ne peut pas dépasser 32 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIRET validation
|
||||||
|
if (form.value.siret && form.value.siret.length > 20) {
|
||||||
|
errors.siret = "Le SIRET ne peut pas dépasser 20 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone validation
|
||||||
|
if (form.value.phone && form.value.phone.length > 50) {
|
||||||
|
errors.phone = "Le téléphone ne peut pas dépasser 50 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address validations
|
||||||
|
if (
|
||||||
|
form.value.billing_address_line1 &&
|
||||||
|
form.value.billing_address_line1.length > 255
|
||||||
|
) {
|
||||||
|
errors.billing_address_line1 =
|
||||||
|
"L'adresse ne peut pas dépasser 255 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
form.value.billing_address_line2 &&
|
||||||
|
form.value.billing_address_line2.length > 255
|
||||||
|
) {
|
||||||
|
errors.billing_address_line2 =
|
||||||
|
"Le complément d'adresse ne peut pas dépasser 255 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
form.value.billing_postal_code &&
|
||||||
|
form.value.billing_postal_code.length > 20
|
||||||
|
) {
|
||||||
|
errors.billing_postal_code =
|
||||||
|
"Le code postal ne peut pas dépasser 20 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.value.billing_city && form.value.billing_city.length > 191) {
|
||||||
|
errors.billing_city = "La ville ne peut pas dépasser 191 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
form.value.billing_country_code &&
|
||||||
|
form.value.billing_country_code.length !== 2
|
||||||
|
) {
|
||||||
|
errors.billing_country_code =
|
||||||
|
"Le code pays doit contenir exactement 2 caractères";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid, errors };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = {
|
||||||
|
name: "",
|
||||||
|
vat_number: "",
|
||||||
|
siret: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
billing_address_line1: "",
|
||||||
|
billing_address_line2: "",
|
||||||
|
billing_postal_code: "",
|
||||||
|
billing_city: "",
|
||||||
|
billing_country_code: "",
|
||||||
|
notes: "",
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
clearErrors();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
errors.value = [];
|
||||||
|
fieldErrors.value = {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f5365c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,457 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Modal Component -->
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
:class="{ show: isVisible, 'd-block': isVisible }"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
:aria-hidden="!isVisible"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
isModification ? 'fas fa-edit me-2' : 'fas fa-user-plus me-2'
|
||||||
|
"
|
||||||
|
></i>
|
||||||
|
{{ isModification ? "Modifier le contact" : "Ajouter un contact" }}
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
@click="closeModal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<!-- Fournisseur ID (hidden) -->
|
||||||
|
<input type="hidden" v-model="formData.fournisseur_id" />
|
||||||
|
|
||||||
|
<!-- First Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Prénom</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.first_name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.first_name }"
|
||||||
|
placeholder="Prénom du contact"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.first_name" class="invalid-feedback">
|
||||||
|
{{ errors.first_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nom</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.last_name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.last_name }"
|
||||||
|
placeholder="Nom du contact"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.last_name" class="invalid-feedback">
|
||||||
|
{{ errors.last_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.email"
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.email }"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.email" class="invalid-feedback">
|
||||||
|
{{ errors.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phone -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Téléphone</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.phone"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.phone }"
|
||||||
|
placeholder="+33 1 23 45 67 89"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.phone" class="invalid-feedback">
|
||||||
|
{{ errors.phone }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Rôle</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.role"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.role }"
|
||||||
|
placeholder="Rôle dans l'entreprise"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="errors.role" class="invalid-feedback">
|
||||||
|
{{ errors.role }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- General Error -->
|
||||||
|
<div v-if="errors.general" class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
{{ errors.general }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="contactIsLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times me-1"></i>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="submitForm"
|
||||||
|
:disabled="contactIsLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save me-1"></i>
|
||||||
|
{{
|
||||||
|
contactIsLoading
|
||||||
|
? isModification
|
||||||
|
? "Modification..."
|
||||||
|
: "Création..."
|
||||||
|
: isModification
|
||||||
|
? "Modifier le contact"
|
||||||
|
: "Créer le contact"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
class="modal-backdrop fade show"
|
||||||
|
@click="closeModal"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, onMounted, onUnmounted } from "vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
fournisseurId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
contactIsLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isModification: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["close", "contact-created", "contact-modified"]);
|
||||||
|
|
||||||
|
// State
|
||||||
|
const errors = reactive({});
|
||||||
|
const formData = reactive({
|
||||||
|
fournisseur_id: "",
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for fournisseurId changes
|
||||||
|
watch(
|
||||||
|
() => props.fournisseurId,
|
||||||
|
(newVal) => {
|
||||||
|
formData.fournisseur_id = newVal;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch for contact changes (for modification mode)
|
||||||
|
watch(
|
||||||
|
() => props.contact,
|
||||||
|
(newContact) => {
|
||||||
|
if (newContact && props.isModification) {
|
||||||
|
formData.first_name = newContact.first_name || "";
|
||||||
|
formData.last_name = newContact.last_name || "";
|
||||||
|
formData.email = newContact.email || "";
|
||||||
|
formData.phone = newContact.phone || "";
|
||||||
|
formData.role = newContact.position || "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const closeModal = () => {
|
||||||
|
resetForm();
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
Object.keys(formData).forEach((key) => {
|
||||||
|
if (key !== "fournisseur_id") {
|
||||||
|
formData[key] = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
// Clear previous errors
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Fournisseur ID validation
|
||||||
|
if (!formData.fournisseur_id) {
|
||||||
|
errors.fournisseur_id = "Le fournisseur est obligatoire.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First name validation
|
||||||
|
if (formData.first_name && formData.first_name.length > 191) {
|
||||||
|
errors.first_name = "Le prénom ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last name validation
|
||||||
|
if (formData.last_name && formData.last_name.length > 191) {
|
||||||
|
errors.last_name = "Le nom ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (formData.email) {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
errors.email = "L'adresse email doit être valide.";
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.email.length > 191) {
|
||||||
|
errors.email = "L'adresse email ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone validation
|
||||||
|
if (formData.phone && formData.phone.length > 50) {
|
||||||
|
errors.phone = "Le téléphone ne peut pas dépasser 50 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role validation
|
||||||
|
if (formData.role && formData.role.length > 191) {
|
||||||
|
errors.role = "Le rôle ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one field validation
|
||||||
|
const hasAtLeastOneField =
|
||||||
|
formData.first_name ||
|
||||||
|
formData.last_name ||
|
||||||
|
formData.email ||
|
||||||
|
formData.phone;
|
||||||
|
|
||||||
|
if (!hasAtLeastOneField) {
|
||||||
|
errors.general =
|
||||||
|
"Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare data for API
|
||||||
|
const submitData = { ...formData };
|
||||||
|
|
||||||
|
// Convert empty strings to null for nullable fields
|
||||||
|
const nullableFields = [
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"phone",
|
||||||
|
"role",
|
||||||
|
];
|
||||||
|
nullableFields.forEach((field) => {
|
||||||
|
if (submitData[field] === "") {
|
||||||
|
submitData[field] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit event - parent will handle the API call and loading state
|
||||||
|
if (props.isModification) {
|
||||||
|
submitData.id = props.contact.id;
|
||||||
|
emit("contact-modified", submitData);
|
||||||
|
} else {
|
||||||
|
emit("contact-created", submitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reset form
|
||||||
|
closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la création du contact:", error);
|
||||||
|
|
||||||
|
// Handle API errors
|
||||||
|
if (error.response && error.response.data && error.response.data.errors) {
|
||||||
|
Object.assign(errors, error.response.data.errors);
|
||||||
|
} else {
|
||||||
|
errors.general =
|
||||||
|
"Une erreur est survenue lors de la création du contact.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard event listener for ESC key
|
||||||
|
const handleKeydown = (event) => {
|
||||||
|
if (event.key === "Escape" && props.isVisible) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener when component is mounted
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border: 1px solid #dce1e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(203, 12, 159, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #cb0c9f;
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: #a90982;
|
||||||
|
border-color: #a90982;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
transition: opacity 0.15s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,21 +1,148 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card contact-list-card">
|
||||||
<div class="card-header pb-0">
|
<div class="card-header pb-0">
|
||||||
<h6 class="mb-0">Contacts du fournisseur</h6>
|
<div class="d-flex align-items-center">
|
||||||
|
<h6 class="mb-0">Liste des contacts</h6>
|
||||||
|
<SoftButton
|
||||||
|
class="btn btn-primary btn-sm ms-auto"
|
||||||
|
@click="contactModalIsVisible = true"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus me-1"></i>Ajouter un contact
|
||||||
|
</SoftButton>
|
||||||
|
</div>
|
||||||
|
<fournisseur-contact-modal
|
||||||
|
:is-visible="contactModalIsVisible"
|
||||||
|
:fournisseur-id="fournisseurId"
|
||||||
|
:contact-is-loading="isLoading"
|
||||||
|
:is-modification="isModification"
|
||||||
|
:contact="selectedContact"
|
||||||
|
@close="closeModal"
|
||||||
|
@contact-created="handleContactCreated"
|
||||||
|
@contact-modified="handleContactModified"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body contact-list-body p-0">
|
||||||
<p class="text-sm">Liste des contacts</p>
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary mt-2">Chargement des contacts...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content State -->
|
||||||
|
<div v-else-if="contacts.length > 0" class="table-responsive">
|
||||||
|
<table class="table align-items-center table-flush mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Téléphone
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
|
||||||
|
>
|
||||||
|
Poste
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-center"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="contact in contacts" :key="contact.id">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex px-2 py-1">
|
||||||
|
<div class="avatar avatar-sm me-3">
|
||||||
|
<div
|
||||||
|
class="avatar-placeholder bg-gradient-info text-white d-flex align-items-center justify-content-center rounded-circle"
|
||||||
|
>
|
||||||
|
{{ getInitials(contact.full_name) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column justify-content-center">
|
||||||
|
<h6 class="mb-0 text-sm">{{ contact.full_name }}</h6>
|
||||||
|
<p
|
||||||
|
v-if="contact.is_primary"
|
||||||
|
class="text-xs text-success mb-0"
|
||||||
|
>
|
||||||
|
<i class="fas fa-star me-1"></i>Contact principal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="text-xs font-weight-bold mb-0">
|
||||||
|
{{ contact.email || "-" }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="text-xs font-weight-bold mb-0">
|
||||||
|
{{ contact.phone || contact.mobile || "-" }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p class="text-xs font-weight-bold mb-0">
|
||||||
|
{{ contact.position || "-" }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-center">
|
||||||
|
<div class="d-flex justify-content-center gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-warning p-0 mb-0"
|
||||||
|
type="button"
|
||||||
|
title="Modifier"
|
||||||
|
@click="handleModifyContact(contact)"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit text-sm"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-danger p-0 mb-0"
|
||||||
|
type="button"
|
||||||
|
title="Supprimer"
|
||||||
|
@click="handleRemoveContact(contact.id)"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="text-center py-5">
|
||||||
|
<i class="fas fa-address-book fa-3x text-secondary opacity-5 mb-3"></i>
|
||||||
|
<p class="text-sm text-secondary">Aucun contact pour ce fournisseur</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps } from "vue";
|
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||||
defineProps({
|
import { useContactStore } from "@/stores/contactStore";
|
||||||
contacts: {
|
import FournisseurContactModal from "./FournisseurContactModal.vue";
|
||||||
type: Array,
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
default: () => [],
|
|
||||||
},
|
const props = defineProps({
|
||||||
fournisseurId: {
|
fournisseurId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true,
|
required: true,
|
||||||
@ -25,4 +152,126 @@ defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use contact store directly to get contacts for this fournisseur
|
||||||
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
|
// Computed property to get filtered contacts for this fournisseur
|
||||||
|
const contacts = computed(() => {
|
||||||
|
// Filter contacts that belong to this fournisseur and add computed full_name
|
||||||
|
return contactStore.contacts
|
||||||
|
.filter((contact) => contact.fournisseur_id === props.fournisseurId)
|
||||||
|
.map((contact) => ({
|
||||||
|
...contact,
|
||||||
|
full_name: `${contact.first_name} ${contact.last_name}`.trim(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactModalIsVisible = ref(false);
|
||||||
|
const isModification = ref(false);
|
||||||
|
const selectedContact = ref(null);
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"contact-created",
|
||||||
|
"contact-modified",
|
||||||
|
"contact-removed",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getInitials = (name) => {
|
||||||
|
if (!name) return "?";
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.substring(0, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContactCreated = (newContact) => {
|
||||||
|
closeModal();
|
||||||
|
emit("contact-created", newContact);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContactModified = (modifiedContact) => {
|
||||||
|
closeModal();
|
||||||
|
emit("contact-modified", modifiedContact);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
contactModalIsVisible.value = false;
|
||||||
|
isModification.value = false;
|
||||||
|
selectedContact.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModifyContact = (contact) => {
|
||||||
|
selectedContact.value = contact;
|
||||||
|
isModification.value = true;
|
||||||
|
contactModalIsVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveContact = async (contactId) => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer ce contact ?")) {
|
||||||
|
try {
|
||||||
|
await contactStore.deleteContact(contactId);
|
||||||
|
emit("contact-removed", contactId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting contact:", error);
|
||||||
|
// You might want to show an error notification here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.contact-list-card {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-flush {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-flush thead th {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-flush tbody tr {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-flush tbody td {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-sm .avatar-placeholder {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state for buttons */
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner styling */
|
||||||
|
.spinner-border {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,21 +1,548 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header pb-0">
|
<div class="card-header pb-0">
|
||||||
<h6 class="mb-0">Informations du fournisseur</h6>
|
<div class="d-flex align-items-center">
|
||||||
|
<h6 class="mb-0">Informations détaillées</h6>
|
||||||
|
<button
|
||||||
|
v-if="!isEditing"
|
||||||
|
class="btn btn-primary btn-sm ms-auto"
|
||||||
|
@click="startEdit"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit me-1"></i>Modifier
|
||||||
|
</button>
|
||||||
|
<div v-else class="ms-auto">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-sm me-2"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times me-1"></i>Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-success btn-sm"
|
||||||
|
@click="saveChanges"
|
||||||
|
:disabled="isSaving"
|
||||||
|
>
|
||||||
|
<i class="fas fa-save me-1"></i>
|
||||||
|
{{ isSaving ? "Enregistrement..." : "Enregistrer" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-sm">Informations détaillées du fournisseur</p>
|
<form @submit.prevent="saveChanges">
|
||||||
<p class="text-xs text-secondary">{{ fournisseur.name }}</p>
|
<!-- Informations générales -->
|
||||||
|
<div class="info-section mb-4">
|
||||||
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
||||||
|
<i class="fas fa-building text-primary me-2"></i>Informations
|
||||||
|
générales
|
||||||
|
</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"
|
||||||
|
>Nom du fournisseur <span class="text-danger">*</span></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.name }"
|
||||||
|
placeholder="Nom du fournisseur"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.name }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.name" class="invalid-feedback d-block">
|
||||||
|
{{ errors.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">SIRET</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.siret"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.siret }"
|
||||||
|
placeholder="SIRET"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.siret || "-" }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.siret" class="invalid-feedback d-block">
|
||||||
|
{{ errors.siret }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Numéro de TVA</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.vat_number"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.vat_number }"
|
||||||
|
placeholder="Numéro de TVA"
|
||||||
|
maxlength="32"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.vat_number || "-" }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.vat_number" class="invalid-feedback d-block">
|
||||||
|
{{ errors.vat_number }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<div v-if="isEditing" class="form-check form-switch mt-2">
|
||||||
|
<input
|
||||||
|
v-model="formData.is_active"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:class="{ 'is-invalid': errors.is_active }"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
{{ formData.is_active ? "Actif" : "Inactif" }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
fournisseur.is_active ? 'text-success' : 'text-danger'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ fournisseur.is_active ? "Actif" : "Inactif" }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.is_active" class="invalid-feedback d-block">
|
||||||
|
{{ errors.is_active }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact -->
|
||||||
|
<div class="info-section mb-4">
|
||||||
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
||||||
|
<i class="fas fa-phone text-success me-2"></i>Contact
|
||||||
|
</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.email"
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.email }"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
<a
|
||||||
|
v-if="fournisseur.email"
|
||||||
|
:href="`mailto:${fournisseur.email}`"
|
||||||
|
>
|
||||||
|
{{ fournisseur.email }}
|
||||||
|
</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.email" class="invalid-feedback d-block">
|
||||||
|
{{ errors.email }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Téléphone</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.phone"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.phone }"
|
||||||
|
placeholder="+33 1 23 45 67 89"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.phone || "-" }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.phone" class="invalid-feedback d-block">
|
||||||
|
{{ errors.phone }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Adresse de facturation -->
|
||||||
|
<div class="info-section mb-4">
|
||||||
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
||||||
|
<i class="fas fa-map-marker-alt text-warning me-2"></i>Adresse de
|
||||||
|
facturation
|
||||||
|
</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Adresse ligne 1</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.billing_address_line1"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.billing_address_line1 }"
|
||||||
|
placeholder="Adresse"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.billing_address?.line1 || "-" }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="errors.billing_address_line1"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
|
{{ errors.billing_address_line1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Adresse ligne 2</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.billing_address_line2"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.billing_address_line2 }"
|
||||||
|
placeholder="Complément d'adresse"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.billing_address?.line2 || "-" }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="errors.billing_address_line2"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
|
{{ errors.billing_address_line2 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code postal</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.billing_postal_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.billing_postal_code }"
|
||||||
|
placeholder="75001"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.billing_address?.postal_code || "-" }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="errors.billing_postal_code"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
|
{{ errors.billing_postal_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Ville</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.billing_city"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.billing_city }"
|
||||||
|
placeholder="Paris"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.billing_address?.city || "-" }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.billing_city" class="invalid-feedback d-block">
|
||||||
|
{{ errors.billing_city }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Pays</label>
|
||||||
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.billing_country_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.billing_country_code }"
|
||||||
|
placeholder="FR"
|
||||||
|
maxlength="2"
|
||||||
|
/>
|
||||||
|
<p v-else class="form-control-static text-sm">
|
||||||
|
{{ fournisseur.billing_address?.country_code || "-" }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="errors.billing_country_code"
|
||||||
|
class="invalid-feedback d-block"
|
||||||
|
>
|
||||||
|
{{ errors.billing_country_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="info-section">
|
||||||
|
<h6 class="text-sm text-uppercase text-body font-weight-bolder mb-3">
|
||||||
|
<i class="fas fa-sticky-note text-info me-2"></i>Notes
|
||||||
|
</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<label class="form-label">Notes internes</label>
|
||||||
|
<textarea
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="formData.notes"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': errors.notes }"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Notes..."
|
||||||
|
></textarea>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="form-control-static text-sm"
|
||||||
|
style="white-space: pre-wrap"
|
||||||
|
>
|
||||||
|
{{ fournisseur.notes || "-" }}
|
||||||
|
</p>
|
||||||
|
<div v-if="errors.notes" class="invalid-feedback d-block">
|
||||||
|
{{ errors.notes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
defineProps({
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
fournisseur: {
|
fournisseur: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["fournisseur-updated"]);
|
||||||
|
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const errors = reactive({});
|
||||||
|
const formData = reactive({
|
||||||
|
name: "",
|
||||||
|
vat_number: "",
|
||||||
|
siret: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
billing_address_line1: "",
|
||||||
|
billing_address_line2: "",
|
||||||
|
billing_postal_code: "",
|
||||||
|
billing_city: "",
|
||||||
|
billing_country_code: "",
|
||||||
|
notes: "",
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
Object.assign(formData, {
|
||||||
|
name: props.fournisseur.name || "",
|
||||||
|
vat_number: props.fournisseur.vat_number || "",
|
||||||
|
siret: props.fournisseur.siret || "",
|
||||||
|
email: props.fournisseur.email || "",
|
||||||
|
phone: props.fournisseur.phone || "",
|
||||||
|
billing_address_line1: props.fournisseur.billing_address?.line1 || "",
|
||||||
|
billing_address_line2: props.fournisseur.billing_address?.line2 || "",
|
||||||
|
billing_postal_code: props.fournisseur.billing_address?.postal_code || "",
|
||||||
|
billing_city: props.fournisseur.billing_address?.city || "",
|
||||||
|
billing_country_code:
|
||||||
|
props.fournisseur.billing_address?.country_code || "FR",
|
||||||
|
notes: props.fournisseur.notes || "",
|
||||||
|
is_active:
|
||||||
|
props.fournisseur.is_active !== undefined
|
||||||
|
? props.fournisseur.is_active
|
||||||
|
: true,
|
||||||
|
});
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditing.value = false;
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Name validation
|
||||||
|
if (!formData.name || formData.name.trim() === "") {
|
||||||
|
errors.name = "Le nom du fournisseur est obligatoire.";
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.name.length > 255) {
|
||||||
|
errors.name = "Le nom du fournisseur ne peut pas dépasser 255 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// VAT number validation
|
||||||
|
if (formData.vat_number && formData.vat_number.length > 32) {
|
||||||
|
errors.vat_number = "Le numéro de TVA ne peut pas dépasser 32 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIRET validation
|
||||||
|
if (formData.siret && formData.siret.length > 20) {
|
||||||
|
errors.siret = "Le SIRET ne peut pas dépasser 20 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation
|
||||||
|
if (formData.email) {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
errors.email = "L'adresse email doit être valide.";
|
||||||
|
isValid = false;
|
||||||
|
} else if (formData.email.length > 191) {
|
||||||
|
errors.email = "L'adresse email ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone validation
|
||||||
|
if (formData.phone && formData.phone.length > 50) {
|
||||||
|
errors.phone = "Le téléphone ne peut pas dépasser 50 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Billing address validations
|
||||||
|
if (
|
||||||
|
formData.billing_address_line1 &&
|
||||||
|
formData.billing_address_line1.length > 255
|
||||||
|
) {
|
||||||
|
errors.billing_address_line1 =
|
||||||
|
"L'adresse ne peut pas dépasser 255 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
formData.billing_address_line2 &&
|
||||||
|
formData.billing_address_line2.length > 255
|
||||||
|
) {
|
||||||
|
errors.billing_address_line2 =
|
||||||
|
"Le complément d'adresse ne peut pas dépasser 255 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
formData.billing_postal_code &&
|
||||||
|
formData.billing_postal_code.length > 20
|
||||||
|
) {
|
||||||
|
errors.billing_postal_code =
|
||||||
|
"Le code postal ne peut pas dépasser 20 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.billing_city && formData.billing_city.length > 191) {
|
||||||
|
errors.billing_city = "La ville ne peut pas dépasser 191 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.billing_country_code) {
|
||||||
|
if (formData.billing_country_code.length !== 2) {
|
||||||
|
errors.billing_country_code = "Le code pays doit contenir 2 caractères.";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChanges = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clean up form data
|
||||||
|
const cleanedData = {};
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (value === "" || value === null || value === undefined) {
|
||||||
|
cleanedData[key] = null;
|
||||||
|
} else {
|
||||||
|
cleanedData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing.value = false;
|
||||||
|
emit("fournisseur-updated", cleanedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la mise à jour:", error);
|
||||||
|
if (error.response && error.response.data && error.response.data.errors) {
|
||||||
|
Object.assign(errors, error.response.data.errors);
|
||||||
|
} else {
|
||||||
|
errors.general = "Une erreur est survenue lors de la sauvegarde.";
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-control-static {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
min-height: calc(1.5em + 1rem);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(203, 12, 159, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: #cb0c9f;
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: #198754 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #cb0c9f;
|
||||||
|
border-color: #cb0c9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #a90982;
|
||||||
|
border-color: #a90982;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
title="Contacts récents"
|
title="Contacts récents"
|
||||||
icon="fas fa-address-book text-info"
|
icon="fas fa-address-book text-info"
|
||||||
>
|
>
|
||||||
<template v-if="contacts.length > 0">
|
<template v-if="filteredContacts.length > 0">
|
||||||
<div class="d-flex align-items-center mb-3 justify-content-end">
|
<div class="d-flex align-items-center mb-3 justify-content-end">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary"
|
class="btn btn-sm btn-outline-primary"
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="contact in contacts.slice(0, 3)"
|
v-for="contact in filteredContacts.slice(0, 3)"
|
||||||
:key="contact.id"
|
:key="contact.id"
|
||||||
class="d-flex align-items-center mb-2"
|
class="d-flex align-items-center mb-2"
|
||||||
>
|
>
|
||||||
@ -109,16 +109,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import InfoCard from "@/components/atoms/client/InfoCard.vue";
|
import InfoCard from "@/components/atoms/client/InfoCard.vue";
|
||||||
import StatusBadge from "@/components/atoms/client/StatusBadge.vue";
|
import StatusBadge from "@/components/atoms/client/StatusBadge.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits, computed } from "vue";
|
||||||
defineProps({
|
import { useContactStore } from "@/stores/contactStore";
|
||||||
|
const props = defineProps({
|
||||||
fournisseur: {
|
fournisseur: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
contacts: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
formattedAddress: {
|
formattedAddress: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "Aucune adresse renseignée",
|
default: "Aucune adresse renseignée",
|
||||||
@ -129,6 +126,15 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use contact store to get filtered contacts for this fournisseur
|
||||||
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
|
const filteredContacts = computed(() => {
|
||||||
|
return contactStore.contacts.filter(
|
||||||
|
(contact) => contact.fournisseur_id === props.fournisseurId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
defineEmits(["view-all-contacts"]);
|
defineEmits(["view-all-contacts"]);
|
||||||
|
|
||||||
const getInitials = (name) => {
|
const getInitials = (name) => {
|
||||||
|
|||||||
@ -26,19 +26,7 @@
|
|||||||
:badge="contactsCount > 0 ? contactsCount : null"
|
:badge="contactsCount > 0 ? contactsCount : null"
|
||||||
@click="$emit('change-tab', 'contacts')"
|
@click="$emit('change-tab', 'contacts')"
|
||||||
/>
|
/>
|
||||||
<TabNavigationItem
|
|
||||||
icon="fas fa-map-marker-alt"
|
|
||||||
label="Adresse"
|
|
||||||
: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
|
<TabNavigationItem
|
||||||
icon="fas fa-sticky-note"
|
icon="fas fa-sticky-note"
|
||||||
label="Notes"
|
label="Notes"
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="multisteps-form mb-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 mx-auto my-5">
|
||||||
|
<slot name="multi-step" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--form panels-->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 m-auto">
|
||||||
|
<slot name="fournisseur-form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -419,6 +419,11 @@ const routes = [
|
|||||||
name: "Gestion fournisseurs",
|
name: "Gestion fournisseurs",
|
||||||
component: () => import("@/views/pages/Fournisseurs/Fournisseurs.vue"),
|
component: () => import("@/views/pages/Fournisseurs/Fournisseurs.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/fournisseurs/new",
|
||||||
|
name: "Creation fournisseur",
|
||||||
|
component: () => import("@/views/pages/Fournisseurs/AddFournisseur.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/fournisseurs/:id",
|
path: "/fournisseurs/:id",
|
||||||
name: "Fournisseur details",
|
name: "Fournisseur details",
|
||||||
|
|||||||
@ -10,6 +10,8 @@ export interface Contact {
|
|||||||
position: string | null;
|
position: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
is_primary: boolean;
|
is_primary: boolean;
|
||||||
|
fournisseur_id: number | null;
|
||||||
|
client_id: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@ -221,6 +223,22 @@ export const ContactService = {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getContactsByFournisseur(
|
||||||
|
fournisseurId: number,
|
||||||
|
params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
}
|
||||||
|
): Promise<ContactListResponse> {
|
||||||
|
const response = await request<ContactListResponse>({
|
||||||
|
url: `/api/fournisseurs/${fournisseurId}/contacts`,
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactService;
|
export default ContactService;
|
||||||
|
|||||||
206
thanasoft-front/src/services/fournisseur.ts
Normal file
206
thanasoft-front/src/services/fournisseur.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
export interface FournisseurAddress {
|
||||||
|
line1: string | null;
|
||||||
|
line2: string | null;
|
||||||
|
postal_code: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country_code: string | null;
|
||||||
|
full_address?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Fournisseur {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
vat_number: string | null;
|
||||||
|
siret: string | null;
|
||||||
|
email: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
billing_address: FournisseurAddress;
|
||||||
|
notes: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
commercial?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FournisseurListResponse {
|
||||||
|
data: Fournisseur[];
|
||||||
|
meta?: {
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FournisseurResponse {
|
||||||
|
data: Fournisseur;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFournisseurPayload {
|
||||||
|
name: string;
|
||||||
|
vat_number?: string | null;
|
||||||
|
siret?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
phone?: string | null;
|
||||||
|
billing_address_line1?: string | null;
|
||||||
|
billing_address_line2?: string | null;
|
||||||
|
billing_postal_code?: string | null;
|
||||||
|
billing_city?: string | null;
|
||||||
|
billing_country_code?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateFournisseurPayload
|
||||||
|
extends Partial<CreateFournisseurPayload> {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FournisseurService = {
|
||||||
|
/**
|
||||||
|
* Get all fournisseurs with pagination
|
||||||
|
*/
|
||||||
|
async getAllFournisseurs(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
}): Promise<FournisseurListResponse> {
|
||||||
|
const response = await request<FournisseurListResponse>({
|
||||||
|
url: "/api/fournisseurs",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific fournisseur by ID
|
||||||
|
*/
|
||||||
|
async getFournisseur(id: number): Promise<FournisseurResponse> {
|
||||||
|
const response = await request<FournisseurResponse>({
|
||||||
|
url: `/api/fournisseurs/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new fournisseur
|
||||||
|
*/
|
||||||
|
async createFournisseur(
|
||||||
|
payload: CreateFournisseurPayload
|
||||||
|
): Promise<FournisseurResponse> {
|
||||||
|
const formattedPayload = this.transformFournisseurPayload(payload);
|
||||||
|
|
||||||
|
const response = await request<FournisseurResponse>({
|
||||||
|
url: "/api/fournisseurs",
|
||||||
|
method: "post",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing fournisseur
|
||||||
|
*/
|
||||||
|
async updateFournisseur(
|
||||||
|
payload: UpdateFournisseurPayload
|
||||||
|
): Promise<FournisseurResponse> {
|
||||||
|
const { id, ...updateData } = payload;
|
||||||
|
const formattedPayload = this.transformFournisseurPayload(updateData);
|
||||||
|
|
||||||
|
const response = await request<FournisseurResponse>({
|
||||||
|
url: `/api/fournisseurs/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a fournisseur
|
||||||
|
*/
|
||||||
|
async deleteFournisseur(
|
||||||
|
id: number
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
const response = await request<{ success: boolean; message: string }>({
|
||||||
|
url: `/api/fournisseurs/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform fournisseur payload to match Laravel form request structure
|
||||||
|
*/
|
||||||
|
transformFournisseurPayload(payload: Partial<CreateFournisseurPayload>): any {
|
||||||
|
const transformed: any = { ...payload };
|
||||||
|
|
||||||
|
// Ensure boolean values are properly formatted
|
||||||
|
if (typeof transformed.is_active === "boolean") {
|
||||||
|
transformed.is_active = transformed.is_active ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove undefined values to avoid sending them
|
||||||
|
Object.keys(transformed).forEach((key) => {
|
||||||
|
if (transformed[key] === undefined) {
|
||||||
|
delete transformed[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformed;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search fournisseurs by name
|
||||||
|
*/
|
||||||
|
async searchFournisseurs(
|
||||||
|
query: string,
|
||||||
|
params?: {
|
||||||
|
exact_match?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<Fournisseur[]> {
|
||||||
|
const response = await request<{
|
||||||
|
data: Fournisseur[];
|
||||||
|
count: number;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/fournisseurs/searchBy",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
name: query,
|
||||||
|
exact_match: params?.exact_match || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active fournisseurs only
|
||||||
|
*/
|
||||||
|
async getActiveFournisseurs(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
}): Promise<FournisseurListResponse> {
|
||||||
|
const response = await request<FournisseurListResponse>({
|
||||||
|
url: "/api/fournisseurs",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
is_active: true,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FournisseurService;
|
||||||
@ -359,6 +359,38 @@ export const useContactStore = defineStore("contact", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchContactsByFournisseur = async (
|
||||||
|
fournisseurId: number,
|
||||||
|
params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await ContactService.getContactsByFournisseur(
|
||||||
|
fournisseurId,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
setContacts(response.data);
|
||||||
|
if (response.meta) {
|
||||||
|
setPagination(response.meta);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Échec du chargement des contacts du client";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get client list contact
|
* Get client list contact
|
||||||
*/
|
*/
|
||||||
@ -434,6 +466,7 @@ export const useContactStore = defineStore("contact", () => {
|
|||||||
fetchContactsByClient,
|
fetchContactsByClient,
|
||||||
resetState,
|
resetState,
|
||||||
getClientListContact,
|
getClientListContact,
|
||||||
|
fetchContactsByFournisseur,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
308
thanasoft-front/src/stores/fournisseurStore.ts
Normal file
308
thanasoft-front/src/stores/fournisseurStore.ts
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import FournisseurService from "@/services/fournisseur";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Fournisseur,
|
||||||
|
CreateFournisseurPayload,
|
||||||
|
UpdateFournisseurPayload,
|
||||||
|
FournisseurListResponse,
|
||||||
|
} from "@/services/fournisseur";
|
||||||
|
|
||||||
|
export const useFournisseurStore = defineStore("fournisseur", () => {
|
||||||
|
// State
|
||||||
|
const fournisseurs = ref<Fournisseur[]>([]);
|
||||||
|
const currentFournisseur = ref<Fournisseur | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const searchResults = ref<Fournisseur[]>([]);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const allFournisseurs = computed(() => fournisseurs.value);
|
||||||
|
const activeFournisseurs = computed(() =>
|
||||||
|
fournisseurs.value.filter((fournisseur) => fournisseur.is_active)
|
||||||
|
);
|
||||||
|
const inactiveFournisseurs = computed(() =>
|
||||||
|
fournisseurs.value.filter((fournisseur) => !fournisseur.is_active)
|
||||||
|
);
|
||||||
|
const isLoading = computed(() => loading.value);
|
||||||
|
const hasError = computed(() => error.value !== null);
|
||||||
|
const getError = computed(() => error.value);
|
||||||
|
const getFournisseurById = computed(() => (id: number) =>
|
||||||
|
fournisseurs.value.find((fournisseur) => fournisseur.id === id)
|
||||||
|
);
|
||||||
|
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 setFournisseurs = (newFournisseurs: Fournisseur[]) => {
|
||||||
|
fournisseurs.value = newFournisseurs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentFournisseur = (fournisseur: Fournisseur | null) => {
|
||||||
|
currentFournisseur.value = fournisseur;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSearchFournisseur = (searchFournisseur: Fournisseur[]) => {
|
||||||
|
searchResults.value = searchFournisseur;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 fournisseurs with optional pagination and filters
|
||||||
|
*/
|
||||||
|
const fetchFournisseurs = async (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FournisseurService.getAllFournisseurs(params);
|
||||||
|
setFournisseurs(response.data);
|
||||||
|
if (response.meta) {
|
||||||
|
setPagination(response.meta);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch fournisseurs";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single fournisseur by ID
|
||||||
|
*/
|
||||||
|
const fetchFournisseur = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FournisseurService.getFournisseur(id);
|
||||||
|
setCurrentFournisseur(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to fetch fournisseur";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new fournisseur
|
||||||
|
*/
|
||||||
|
const createFournisseur = async (payload: CreateFournisseurPayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FournisseurService.createFournisseur(payload);
|
||||||
|
// Add the new fournisseur to the list
|
||||||
|
fournisseurs.value.push(response.data);
|
||||||
|
setCurrentFournisseur(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to create fournisseur";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing fournisseur
|
||||||
|
*/
|
||||||
|
const updateFournisseur = async (payload: UpdateFournisseurPayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FournisseurService.updateFournisseur(payload);
|
||||||
|
const updatedFournisseur = response.data;
|
||||||
|
|
||||||
|
// Update in the fournisseurs list
|
||||||
|
const index = fournisseurs.value.findIndex(
|
||||||
|
(fournisseur) => fournisseur.id === updatedFournisseur.id
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
fournisseurs.value[index] = updatedFournisseur;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current fournisseur if it's the one being edited
|
||||||
|
if (
|
||||||
|
currentFournisseur.value &&
|
||||||
|
currentFournisseur.value.id === updatedFournisseur.id
|
||||||
|
) {
|
||||||
|
setCurrentFournisseur(updatedFournisseur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedFournisseur;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to update fournisseur";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a fournisseur
|
||||||
|
*/
|
||||||
|
const deleteFournisseur = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FournisseurService.deleteFournisseur(id);
|
||||||
|
|
||||||
|
// Remove from the fournisseurs list
|
||||||
|
fournisseurs.value = fournisseurs.value.filter(
|
||||||
|
(fournisseur) => fournisseur.id !== id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear current fournisseur if it's the one being deleted
|
||||||
|
if (currentFournisseur.value && currentFournisseur.value.id === id) {
|
||||||
|
setCurrentFournisseur(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message ||
|
||||||
|
err.message ||
|
||||||
|
"Failed to delete fournisseur";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search fournisseurs
|
||||||
|
*/
|
||||||
|
const searchFournisseurs = async (
|
||||||
|
query: string,
|
||||||
|
exactMatch: boolean = false
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await FournisseurService.searchFournisseurs(query, {
|
||||||
|
exact_match: exactMatch,
|
||||||
|
});
|
||||||
|
setSearchFournisseur(results);
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
error.value = "Erreur lors de la recherche des fournisseurs";
|
||||||
|
console.error("Error searching fournisseurs:", err);
|
||||||
|
setSearchFournisseur([]);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear current fournisseur
|
||||||
|
*/
|
||||||
|
const clearCurrentFournisseur = () => {
|
||||||
|
setCurrentFournisseur(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all state
|
||||||
|
*/
|
||||||
|
const clearStore = () => {
|
||||||
|
fournisseurs.value = [];
|
||||||
|
currentFournisseur.value = null;
|
||||||
|
error.value = null;
|
||||||
|
pagination.value = {
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
fournisseurs,
|
||||||
|
currentFournisseur,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
allFournisseurs,
|
||||||
|
activeFournisseurs,
|
||||||
|
inactiveFournisseurs,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
getError,
|
||||||
|
getFournisseurById,
|
||||||
|
getPagination,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchFournisseurs,
|
||||||
|
fetchFournisseur,
|
||||||
|
createFournisseur,
|
||||||
|
updateFournisseur,
|
||||||
|
deleteFournisseur,
|
||||||
|
searchFournisseurs,
|
||||||
|
clearCurrentFournisseur,
|
||||||
|
clearStore,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -11,12 +11,14 @@
|
|||||||
import AddClientPresentation from "@/components/Organism/CRM/AddClientPresentation.vue";
|
import AddClientPresentation from "@/components/Organism/CRM/AddClientPresentation.vue";
|
||||||
import { useClientCategoryStore } from "@/stores/clientCategorie.store";
|
import { useClientCategoryStore } from "@/stores/clientCategorie.store";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const categoryClientStore = useClientCategoryStore();
|
const categoryClientStore = useClientCategoryStore();
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
const validationErrors = ref({});
|
const validationErrors = ref({});
|
||||||
const showSuccess = ref(false);
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
@ -33,12 +35,13 @@ const handleCreateClient = async (form) => {
|
|||||||
// Call the store to create client
|
// Call the store to create client
|
||||||
const client = await clientStore.createClient(form);
|
const client = await clientStore.createClient(form);
|
||||||
|
|
||||||
// Show success message
|
// Show success notification
|
||||||
|
notificationStore.created("Client");
|
||||||
showSuccess.value = true;
|
showSuccess.value = true;
|
||||||
|
|
||||||
// Redirect after 2 seconds
|
// Redirect after 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push({ name: "Clients" });
|
router.push({ name: "Gestion clients" });
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating client:", error);
|
console.error("Error creating client:", error);
|
||||||
@ -46,13 +49,17 @@ const handleCreateClient = async (form) => {
|
|||||||
// Handle validation errors from Laravel
|
// Handle validation errors from Laravel
|
||||||
if (error.response && error.response.status === 422) {
|
if (error.response && error.response.status === 422) {
|
||||||
validationErrors.value = error.response.data.errors || {};
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
} else if (error.response && error.response.data) {
|
} else if (error.response && error.response.data) {
|
||||||
// Handle other API errors
|
// Handle other API errors
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.response.data.message || "Une erreur est survenue";
|
error.response.data.message || "Une erreur est survenue";
|
||||||
alert(errorMessage);
|
notificationStore.error("Erreur", errorMessage);
|
||||||
} else {
|
} else {
|
||||||
alert("Une erreur inattendue s'est produite");
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,18 +9,38 @@
|
|||||||
import AddContactPresentation from "@/components/Organism/CRM/contact/AddContactPresentation.vue";
|
import AddContactPresentation from "@/components/Organism/CRM/contact/AddContactPresentation.vue";
|
||||||
import useContactStore from "@/stores/contactStore";
|
import useContactStore from "@/stores/contactStore";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const contactStore = useContactStore();
|
const contactStore = useContactStore();
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const resultSeach = ref([]);
|
const resultSeach = ref([]);
|
||||||
|
|
||||||
const handleCreateContact = async (contactData) => {
|
const handleCreateContact = async (contactData) => {
|
||||||
await contactStore.createContact(contactData);
|
try {
|
||||||
|
await contactStore.createContact(contactData);
|
||||||
|
notificationStore.created("Contact");
|
||||||
|
|
||||||
|
// Redirect after success
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Gestion contacts" });
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating contact:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de créer le contact");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchClient = async (searchInput) => {
|
const handleSearchClient = async (searchInput) => {
|
||||||
resultSeach.value = await clientStore.searchClients(searchInput);
|
try {
|
||||||
|
resultSeach.value = await clientStore.searchClients(searchInput);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching clients:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de rechercher les clients");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -25,12 +25,14 @@ 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 { useClientLocationStore } from "@/stores/clientLocation";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
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();
|
const clientLocationStore = useClientLocationStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
// 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);
|
||||||
@ -54,15 +56,22 @@ onMounted(async () => {
|
|||||||
const updateClient = async (data) => {
|
const updateClient = async (data) => {
|
||||||
if (!client_id) {
|
if (!client_id) {
|
||||||
console.error("Missing client id");
|
console.error("Missing client id");
|
||||||
|
notificationStore.error("Erreur", "ID du client manquant");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If data is FormData (e.g. file upload), append the id instead of spreading
|
try {
|
||||||
if (data instanceof FormData) {
|
// If data is FormData (e.g. file upload), append the id instead of spreading
|
||||||
data.set("id", String(client_id));
|
if (data instanceof FormData) {
|
||||||
await clientStore.updateClient(data);
|
data.set("id", String(client_id));
|
||||||
} else {
|
await clientStore.updateClient(data);
|
||||||
await clientStore.updateClient({ id: client_id, ...data });
|
} else {
|
||||||
|
await clientStore.updateClient({ id: client_id, ...data });
|
||||||
|
}
|
||||||
|
notificationStore.updated("Client");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating client:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de mettre à jour le client");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,8 +80,10 @@ const createNewContact = async (data) => {
|
|||||||
await contactStore.createContact(data);
|
await contactStore.createContact(data);
|
||||||
// Refresh contacts list after creation
|
// Refresh contacts list after creation
|
||||||
contacts_client.value = await contactStore.getClientListContact(client_id);
|
contacts_client.value = await contactStore.getClientListContact(client_id);
|
||||||
|
notificationStore.created("Contact");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating contact:", error);
|
console.error("Error creating contact:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de créer le contact");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,8 +91,10 @@ const updateContact = async (modifiedContact) => {
|
|||||||
try {
|
try {
|
||||||
await contactStore.updateContact(modifiedContact);
|
await contactStore.updateContact(modifiedContact);
|
||||||
contacts_client.value = await contactStore.getClientListContact(client_id);
|
contacts_client.value = await contactStore.getClientListContact(client_id);
|
||||||
|
notificationStore.updated("Contact");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating contact:", error);
|
console.error("Error updating contact:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de modifier le contact");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,8 +104,10 @@ const createNewLocation = async (data) => {
|
|||||||
// Refresh locations list after creation
|
// Refresh locations list after creation
|
||||||
const response = await clientLocationStore.getClientLocations(client_id);
|
const response = await clientLocationStore.getClientLocations(client_id);
|
||||||
locations_client.value = response || [];
|
locations_client.value = response || [];
|
||||||
|
notificationStore.created("Localisation");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating location:", error);
|
console.error("Error creating location:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de créer la localisation");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -102,8 +117,10 @@ const modifyLocation = async (location) => {
|
|||||||
// Refresh locations list after modification
|
// Refresh locations list after modification
|
||||||
const response = await clientLocationStore.getClientLocations(client_id);
|
const response = await clientLocationStore.getClientLocations(client_id);
|
||||||
locations_client.value = response || [];
|
locations_client.value = response || [];
|
||||||
|
notificationStore.updated("Localisation");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error modifying location:", error);
|
console.error("Error modifying location:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de modifier la localisation");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -113,8 +130,13 @@ const removeLocation = async (locationId) => {
|
|||||||
// Refresh locations list after deletion
|
// Refresh locations list after deletion
|
||||||
const response = await clientLocationStore.getClientLocations(client_id);
|
const response = await clientLocationStore.getClientLocations(client_id);
|
||||||
locations_client.value = response || [];
|
locations_client.value = response || [];
|
||||||
|
notificationStore.deleted("Localisation");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error removing location:", error);
|
console.error("Error removing location:", error);
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de supprimer la localisation"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<add-fournisseur-presentation
|
||||||
|
:loading="fournisseurStore.isLoading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="showSuccess"
|
||||||
|
@create-fournisseur="handleCreateFournisseur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import AddFournisseurPresentation from "@/components/Organism/CRM/AddFournisseurPresentation.vue";
|
||||||
|
import { useFournisseurStore } from "@/stores/fournisseurStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const fournisseurStore = useFournisseurStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
|
const handleCreateFournisseur = async (form) => {
|
||||||
|
try {
|
||||||
|
// Clear previous errors
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
|
||||||
|
// Call the store to create fournisseur
|
||||||
|
const fournisseur = await fournisseurStore.createFournisseur(form);
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
notificationStore.created("Fournisseur");
|
||||||
|
showSuccess.value = true;
|
||||||
|
|
||||||
|
// Redirect after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Gestion fournisseurs" });
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating fournisseur:", error);
|
||||||
|
|
||||||
|
// Handle validation errors from Laravel
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
// Handle other API errors
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data.message || "Une erreur est survenue";
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<fournisseur-detail-presentation
|
<fournisseur-detail-presentation
|
||||||
v-if="currentFournisseur"
|
v-if="fournisseurStore.currentFournisseur"
|
||||||
:fournisseur="currentFournisseur"
|
:fournisseur="fournisseurStore.currentFournisseur"
|
||||||
:contacts="contacts_fournisseur"
|
|
||||||
:locations="locations_fournisseur"
|
:locations="locations_fournisseur"
|
||||||
:is-loading="isLoading"
|
:is-loading="fournisseurStore.isLoading"
|
||||||
:fournisseur-avatar="fournisseurAvatar"
|
:fournisseur-avatar="fournisseurAvatar"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
:file-input="fileInput"
|
:file-input="fileInput"
|
||||||
:contact-loading="contactLoading"
|
:contact-loading="contactStore.isLoading"
|
||||||
:location-loading="locationLoading"
|
:location-loading="locationLoading"
|
||||||
@update-the-fournisseur="updateFournisseur"
|
@update-the-fournisseur="updateFournisseur"
|
||||||
@add-new-contact="createNewContact"
|
@add-new-contact="createNewContact"
|
||||||
@updating-contact="updateContact"
|
@updating-contact="updateContact"
|
||||||
|
@contact-removed="handleContactRemoved"
|
||||||
@add-new-location="createNewLocation"
|
@add-new-location="createNewLocation"
|
||||||
@modify-location="modifyLocation"
|
@modify-location="modifyLocation"
|
||||||
@remove-location="removeLocation"
|
@remove-location="removeLocation"
|
||||||
@ -22,126 +22,119 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
import { useFournisseurStore } from "@/stores/fournisseurStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { useContactStore } from "@/stores/contactStore";
|
||||||
import FournisseurDetailPresentation from "@/components/Organism/CRM/FournisseurDetailPresentation.vue";
|
import FournisseurDetailPresentation from "@/components/Organism/CRM/FournisseurDetailPresentation.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const fournisseurStore = useFournisseurStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
// Ensure fournisseur_id is a number
|
// Ensure fournisseur_id is a number
|
||||||
const fournisseur_id = Number(route.params.id);
|
const fournisseur_id = Number(route.params.id);
|
||||||
const contacts_fournisseur = ref([]);
|
|
||||||
const locations_fournisseur = ref([]);
|
const locations_fournisseur = ref([]);
|
||||||
const activeTab = ref("overview");
|
const activeTab = ref("overview");
|
||||||
const fournisseurAvatar = ref(null);
|
const fournisseurAvatar = ref(null);
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
const isLoading = ref(false);
|
|
||||||
const contactLoading = ref(false);
|
|
||||||
const locationLoading = ref(false);
|
const locationLoading = ref(false);
|
||||||
|
|
||||||
// Dummy fournisseur data
|
|
||||||
const currentFournisseur = ref({
|
|
||||||
id: fournisseur_id,
|
|
||||||
name: "Fournisseur Alpha",
|
|
||||||
commercial: "Jean Dupont",
|
|
||||||
billing_address: {
|
|
||||||
line1: "123 Rue de la Paix",
|
|
||||||
line2: "Bâtiment A",
|
|
||||||
postal_code: "75001",
|
|
||||||
city: "Paris",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Entreprise",
|
|
||||||
email: "contact@alpha.fr",
|
|
||||||
phone: "+33 1 23 45 67 89",
|
|
||||||
is_active: true,
|
|
||||||
siret: "12345678901234",
|
|
||||||
tva_number: "FR12345678901",
|
|
||||||
payment_terms: "30 jours",
|
|
||||||
notes: "Fournisseur principal pour les matériaux de construction",
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Dummy contacts data
|
if (fournisseur_id) {
|
||||||
contacts_fournisseur.value = [
|
// Load contacts for this fournisseur
|
||||||
{
|
await contactStore.fetchContactsByFournisseur(fournisseur_id);
|
||||||
id: 1,
|
await fournisseurStore.fetchFournisseur(fournisseur_id);
|
||||||
first_name: "Pierre",
|
|
||||||
last_name: "Martin",
|
|
||||||
email: "pierre.martin@alpha.fr",
|
|
||||||
phone: "+33 1 23 45 67 90",
|
|
||||||
position: "Responsable commercial",
|
|
||||||
is_primary: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
first_name: "Sophie",
|
|
||||||
last_name: "Dubois",
|
|
||||||
email: "sophie.dubois@alpha.fr",
|
|
||||||
phone: "+33 1 23 45 67 91",
|
|
||||||
position: "Assistante",
|
|
||||||
is_primary: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Dummy locations data
|
// TODO: Fetch locations when API is ready
|
||||||
locations_fournisseur.value = [
|
// locations_fournisseur.value = await locationStore.getFournisseurLocations(fournisseur_id);
|
||||||
{
|
}
|
||||||
id: 1,
|
|
||||||
name: "Siège social",
|
|
||||||
address: {
|
|
||||||
line1: "123 Rue de la Paix",
|
|
||||||
line2: "Bâtiment A",
|
|
||||||
postal_code: "75001",
|
|
||||||
city: "Paris",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
gps_lat: "48.8566",
|
|
||||||
gps_lng: "2.3522",
|
|
||||||
is_default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Entrepôt",
|
|
||||||
address: {
|
|
||||||
line1: "456 Avenue des Champs",
|
|
||||||
line2: null,
|
|
||||||
postal_code: "93100",
|
|
||||||
city: "Montreuil",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
gps_lat: "48.8634",
|
|
||||||
gps_lng: "2.4411",
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateFournisseur = async (data) => {
|
const updateFournisseur = async (data) => {
|
||||||
console.log("Update fournisseur:", data);
|
if (!fournisseur_id) {
|
||||||
// TODO: Implement update logic with store
|
console.error("Missing fournisseur id");
|
||||||
|
notificationStore.error("Erreur", "ID du fournisseur manquant");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fournisseurStore.updateFournisseur({ id: fournisseur_id, ...data });
|
||||||
|
notificationStore.updated("Fournisseur");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating fournisseur:", error);
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de mettre à jour le fournisseur"
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewContact = async (data) => {
|
const createNewContact = async (data) => {
|
||||||
console.log("Create new contact:", data);
|
try {
|
||||||
// TODO: Implement create contact logic
|
// Ensure fournisseur_id is set
|
||||||
|
const contactData = {
|
||||||
|
...data,
|
||||||
|
fournisseur_id: fournisseur_id,
|
||||||
|
client_id: null, // Explicitly set to null for fournisseur contacts
|
||||||
|
};
|
||||||
|
|
||||||
|
await contactStore.createContact(contactData);
|
||||||
|
|
||||||
|
notificationStore.created("Contact");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating contact:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de créer le contact");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateContact = async (modifiedContact) => {
|
const updateContact = async (modifiedContact) => {
|
||||||
console.log("Update contact:", modifiedContact);
|
try {
|
||||||
// TODO: Implement update contact logic
|
await contactStore.updateContact(modifiedContact);
|
||||||
|
|
||||||
|
notificationStore.updated("Contact");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating contact:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de modifier le contact");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContactRemoved = (contactId) => {
|
||||||
|
// The contact is already removed from the store
|
||||||
|
// No additional action needed as the display will update automatically
|
||||||
|
notificationStore.deleted("Contact");
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewLocation = async (data) => {
|
const createNewLocation = async (data) => {
|
||||||
console.log("Create new location:", data);
|
try {
|
||||||
// TODO: Implement create location logic
|
console.log("Create new location:", data);
|
||||||
|
// TODO: Implement with location store when ready
|
||||||
|
notificationStore.created("Localisation");
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.error("Erreur", "Impossible de créer la localisation");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyLocation = async (location) => {
|
const modifyLocation = async (location) => {
|
||||||
console.log("Modify location:", location);
|
try {
|
||||||
// TODO: Implement modify location logic
|
console.log("Modify location:", location);
|
||||||
|
// TODO: Implement with location store when ready
|
||||||
|
notificationStore.updated("Localisation");
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.error("Erreur", "Impossible de modifier la localisation");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLocation = async (locationId) => {
|
const removeLocation = async (locationId) => {
|
||||||
console.log("Remove location:", locationId);
|
try {
|
||||||
// TODO: Implement remove location logic
|
console.log("Remove location:", locationId);
|
||||||
|
// TODO: Implement with location store when ready
|
||||||
|
notificationStore.deleted("Localisation");
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de supprimer la localisation"
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,104 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<fournisseur-presentation
|
<fournisseur-presentation
|
||||||
:fournisseur-data="fournisseurs"
|
:fournisseur-data="fournisseurStore.fournisseurs"
|
||||||
:loading-data="loading"
|
:loading-data="fournisseurStore.loading"
|
||||||
@push-details="goDetails"
|
@push-details="goDetails"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import FournisseurPresentation from "@/components/Organism/CRM/FournisseurPresentation.vue";
|
import FournisseurPresentation from "@/components/Organism/CRM/FournisseurPresentation.vue";
|
||||||
import { ref } from "vue";
|
import { useFournisseurStore } from "@/stores/fournisseurStore";
|
||||||
|
import { onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const fournisseurStore = useFournisseurStore();
|
||||||
|
|
||||||
// Dummy data for fournisseurs
|
onMounted(async () => {
|
||||||
const fournisseurs = ref([
|
await fournisseurStore.fetchFournisseurs();
|
||||||
{
|
});
|
||||||
id: 1,
|
|
||||||
name: "Fournisseur Alpha",
|
|
||||||
commercial: "Jean Dupont",
|
|
||||||
billing_address: {
|
|
||||||
line1: "123 Rue de la Paix",
|
|
||||||
postal_code: "75001",
|
|
||||||
city: "Paris",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Entreprise",
|
|
||||||
email: "contact@alpha.fr",
|
|
||||||
phone: "+33 1 23 45 67 89",
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Fournisseur Beta",
|
|
||||||
commercial: "Marie Martin",
|
|
||||||
billing_address: {
|
|
||||||
line1: "456 Avenue des Champs",
|
|
||||||
postal_code: "69001",
|
|
||||||
city: "Lyon",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Entreprise",
|
|
||||||
email: "info@beta.fr",
|
|
||||||
phone: "+33 4 78 90 12 34",
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Fournisseur Gamma",
|
|
||||||
commercial: "Pierre Dubois",
|
|
||||||
billing_address: {
|
|
||||||
line1: "789 Boulevard Victor Hugo",
|
|
||||||
postal_code: "13001",
|
|
||||||
city: "Marseille",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Association",
|
|
||||||
email: "contact@gamma.org",
|
|
||||||
phone: "+33 4 91 23 45 67",
|
|
||||||
is_active: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Fournisseur Delta",
|
|
||||||
commercial: "Sophie Laurent",
|
|
||||||
billing_address: {
|
|
||||||
line1: "321 Rue de la République",
|
|
||||||
postal_code: "33000",
|
|
||||||
city: "Bordeaux",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Entreprise",
|
|
||||||
email: "hello@delta.fr",
|
|
||||||
phone: "+33 5 56 78 90 12",
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "Fournisseur Epsilon",
|
|
||||||
commercial: "Luc Bernard",
|
|
||||||
billing_address: {
|
|
||||||
line1: "654 Avenue de la Liberté",
|
|
||||||
postal_code: "59000",
|
|
||||||
city: "Lille",
|
|
||||||
country_code: "FR",
|
|
||||||
},
|
|
||||||
type_label: "Particulier",
|
|
||||||
email: "contact@epsilon.fr",
|
|
||||||
phone: "+33 3 20 12 34 56",
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const goDetails = (id) => {
|
const goDetails = (id) => {
|
||||||
console.log("Navigate to fournisseur details:", id);
|
router.push({
|
||||||
// router.push({
|
name: "Fournisseur details",
|
||||||
// name: "Fournisseur details",
|
params: {
|
||||||
// params: {
|
id: id,
|
||||||
// id: id,
|
},
|
||||||
// },
|
});
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user