add notification et crud fournisseur
This commit is contained in:
parent
ca09f6da2f
commit
4b056038d6
@ -187,4 +187,28 @@ class ContactController extends Controller
|
||||
], 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
|
||||
{
|
||||
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',
|
||||
'last_name' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|email|max:191',
|
||||
@ -34,8 +35,8 @@ class StoreContactRequest extends FormRequest
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.required' => 'Le client est obligatoire.',
|
||||
'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.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.',
|
||||
@ -50,9 +51,17 @@ class StoreContactRequest extends FormRequest
|
||||
public function withValidator($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)) {
|
||||
$validator->errors()->add(
|
||||
'general',
|
||||
'general',
|
||||
'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
|
||||
{
|
||||
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',
|
||||
'last_name' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|email|max:191',
|
||||
@ -34,8 +35,8 @@ class UpdateContactRequest extends FormRequest
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.required' => 'Le client est obligatoire.',
|
||||
'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.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.',
|
||||
@ -51,9 +52,17 @@ class UpdateContactRequest extends FormRequest
|
||||
public function withValidator($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)) {
|
||||
$validator->errors()->add(
|
||||
'general',
|
||||
'general',
|
||||
'Au moins un champ (prénom, nom, email ou téléphone) doit être renseigné.'
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ class ContactResource extends JsonResource
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'client_id' => $this->client_id,
|
||||
'fournisseur_id' => $this->fournisseur_id,
|
||||
'first_name' => $this->first_name,
|
||||
'last_name' => $this->last_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'),
|
||||
|
||||
// Relations
|
||||
'client' => $this->whenLoaded('client', [
|
||||
'id' => $this->client->id,
|
||||
'name' => $this->client->name,
|
||||
]),
|
||||
'client' => $this->whenLoaded('client', function() {
|
||||
return $this->client ? [
|
||||
'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',
|
||||
'notes',
|
||||
'is_primary',
|
||||
'client_id'
|
||||
'client_id',
|
||||
'fournisseur_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@ -28,6 +29,11 @@ class Contact extends Model
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function fournisseur(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Fournisseur::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contact's full name.
|
||||
*/
|
||||
|
||||
@ -62,4 +62,11 @@ class ContactRepository extends BaseRepository implements ContactRepositoryInter
|
||||
->where('client_id', $clientId)
|
||||
->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 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
|
||||
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
||||
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)"
|
||||
:fournisseur-name="fournisseur.name"
|
||||
:fournisseur-type="fournisseur.type_label || 'Fournisseur'"
|
||||
:contacts-count="contacts.length"
|
||||
:contacts-count="filteredContactsCount"
|
||||
:locations-count="locations.length"
|
||||
:is-active="fournisseur.is_active"
|
||||
:active-tab="activeTab"
|
||||
@ -41,10 +41,9 @@
|
||||
/>
|
||||
</template>
|
||||
<template #fournisseur-detail-content>
|
||||
<FournisseurDetailContent
|
||||
<fournisseur-detail-content
|
||||
:active-tab="activeTab"
|
||||
:fournisseur="fournisseur"
|
||||
:contacts="contacts"
|
||||
:locations="locations"
|
||||
:formatted-address="formatAddress(fournisseur)"
|
||||
:fournisseur-id="fournisseur.id"
|
||||
@ -54,6 +53,7 @@
|
||||
@updating-fournisseur="handleUpdateFournisseur"
|
||||
@create-contact="handleAddContact"
|
||||
@updating-contact="handleModifiedContact"
|
||||
@contact-removed="handleRemovedContact"
|
||||
@create-location="handleAddLocation"
|
||||
@modify-location="handleModifyLocation"
|
||||
@remove-location="handleRemoveLocation"
|
||||
@ -62,7 +62,8 @@
|
||||
</fournisseur-detail-template>
|
||||
</template>
|
||||
<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 FournisseurDetailSidebar from "./fournisseur/FournisseurDetailSidebar.vue";
|
||||
import FournisseurDetailContent from "./fournisseur/FournisseurDetailContent.vue";
|
||||
@ -73,11 +74,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
contacts: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@ -87,6 +83,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
contactIsLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fournisseurAvatar: {
|
||||
type: String,
|
||||
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 emit = defineEmits([
|
||||
@ -116,6 +125,7 @@ const emit = defineEmits([
|
||||
"handleFileInput",
|
||||
"add-new-contact",
|
||||
"updating-contact",
|
||||
"contact-removed",
|
||||
"add-new-location",
|
||||
"modify-location",
|
||||
"remove-location",
|
||||
@ -150,6 +160,10 @@ const handleModifiedContact = (modifiedContact) => {
|
||||
emit("updating-contact", modifiedContact);
|
||||
};
|
||||
|
||||
const handleRemovedContact = (contactId) => {
|
||||
emit("contact-removed", contactId);
|
||||
};
|
||||
|
||||
const handleAddLocation = (data) => {
|
||||
emit("add-new-location", data);
|
||||
};
|
||||
|
||||
@ -44,8 +44,9 @@ defineProps({
|
||||
});
|
||||
|
||||
const goToFournisseur = () => {
|
||||
// Navigate to create fournisseur page when implemented
|
||||
console.log("Navigate to create fournisseur");
|
||||
router.push({
|
||||
name: "Creation fournisseur",
|
||||
});
|
||||
};
|
||||
|
||||
const goToDetails = (fournisseurId) => {
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
<div v-show="activeTab === 'overview'">
|
||||
<FournisseurOverview
|
||||
:fournisseur="fournisseur"
|
||||
:contacts="contacts"
|
||||
:formatted-address="formattedAddress"
|
||||
:fournisseur-id="fournisseurId"
|
||||
@view-all-contacts="$emit('change-tab', 'contacts')"
|
||||
@ -26,11 +25,11 @@
|
||||
<!-- Contacts Tab -->
|
||||
<div v-show="activeTab === 'contacts'">
|
||||
<FournisseurContactsTab
|
||||
:contacts="contacts"
|
||||
:fournisseur-id="fournisseur.id"
|
||||
:is-loading="contactIsLoading"
|
||||
@contact-created="handleCreateContact"
|
||||
@contact-modified="handleModifiedContact"
|
||||
@contact-removed="handleRemovedContact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -77,10 +76,6 @@ defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@ -111,6 +106,7 @@ const emit = defineEmits([
|
||||
"modify-location",
|
||||
"remove-location",
|
||||
"updating-contact",
|
||||
"contact-removed",
|
||||
]);
|
||||
|
||||
const updateFournisseur = (updatedFournisseur) => {
|
||||
@ -136,4 +132,10 @@ const handleModifyLocation = (location) => {
|
||||
const handleRemoveLocation = (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>
|
||||
|
||||
@ -3,19 +3,6 @@
|
||||
<h5 class="font-weight-bolder mb-0">Nouveau Client</h5>
|
||||
<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">
|
||||
<!-- Catégorie du client -->
|
||||
<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>
|
||||
<div class="card">
|
||||
<div class="card contact-list-card">
|
||||
<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 class="card-body">
|
||||
<p class="text-sm">Liste des contacts</p>
|
||||
<div class="card-body contact-list-body p-0">
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||
import { useContactStore } from "@/stores/contactStore";
|
||||
import FournisseurContactModal from "./FournisseurContactModal.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
fournisseurId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
@ -25,4 +152,126 @@ defineProps({
|
||||
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>
|
||||
|
||||
<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>
|
||||
<div class="card">
|
||||
<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 class="card-body">
|
||||
<p class="text-sm">Informations détaillées du fournisseur</p>
|
||||
<p class="text-xs text-secondary">{{ fournisseur.name }}</p>
|
||||
<form @submit.prevent="saveChanges">
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
defineProps({
|
||||
import { ref, reactive } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
fournisseur: {
|
||||
type: Object,
|
||||
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>
|
||||
|
||||
<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"
|
||||
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">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
@ -66,7 +66,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="contact in contacts.slice(0, 3)"
|
||||
v-for="contact in filteredContacts.slice(0, 3)"
|
||||
:key="contact.id"
|
||||
class="d-flex align-items-center mb-2"
|
||||
>
|
||||
@ -109,16 +109,13 @@
|
||||
<script setup>
|
||||
import InfoCard from "@/components/atoms/client/InfoCard.vue";
|
||||
import StatusBadge from "@/components/atoms/client/StatusBadge.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
defineProps({
|
||||
import { defineProps, defineEmits, computed } from "vue";
|
||||
import { useContactStore } from "@/stores/contactStore";
|
||||
const props = defineProps({
|
||||
fournisseur: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
formattedAddress: {
|
||||
type: String,
|
||||
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"]);
|
||||
|
||||
const getInitials = (name) => {
|
||||
|
||||
@ -26,19 +26,7 @@
|
||||
:badge="contactsCount > 0 ? contactsCount : null"
|
||||
@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
|
||||
icon="fas fa-sticky-note"
|
||||
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",
|
||||
component: () => import("@/views/pages/Fournisseurs/Fournisseurs.vue"),
|
||||
},
|
||||
{
|
||||
path: "/fournisseurs/new",
|
||||
name: "Creation fournisseur",
|
||||
component: () => import("@/views/pages/Fournisseurs/AddFournisseur.vue"),
|
||||
},
|
||||
{
|
||||
path: "/fournisseurs/:id",
|
||||
name: "Fournisseur details",
|
||||
|
||||
@ -10,6 +10,8 @@ export interface Contact {
|
||||
position: string | null;
|
||||
notes: string | null;
|
||||
is_primary: boolean;
|
||||
fournisseur_id: number | null;
|
||||
client_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@ -221,6 +223,22 @@ export const ContactService = {
|
||||
|
||||
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
|
||||
*/
|
||||
@ -434,6 +466,7 @@ export const useContactStore = defineStore("contact", () => {
|
||||
fetchContactsByClient,
|
||||
resetState,
|
||||
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 { useClientCategoryStore } from "@/stores/clientCategorie.store";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const categoryClientStore = useClientCategoryStore();
|
||||
const clientStore = useClientStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const validationErrors = ref({});
|
||||
const showSuccess = ref(false);
|
||||
|
||||
@ -33,12 +35,13 @@ const handleCreateClient = async (form) => {
|
||||
// Call the store to create client
|
||||
const client = await clientStore.createClient(form);
|
||||
|
||||
// Show success message
|
||||
// Show success notification
|
||||
notificationStore.created("Client");
|
||||
showSuccess.value = true;
|
||||
|
||||
// Redirect after 2 seconds
|
||||
setTimeout(() => {
|
||||
router.push({ name: "Clients" });
|
||||
router.push({ name: "Gestion clients" });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error creating client:", error);
|
||||
@ -46,13 +49,17 @@ const handleCreateClient = async (form) => {
|
||||
// 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";
|
||||
alert(errorMessage);
|
||||
notificationStore.error("Erreur", errorMessage);
|
||||
} 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 useContactStore from "@/stores/contactStore";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const contactStore = useContactStore();
|
||||
const clientStore = useClientStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const router = useRouter();
|
||||
|
||||
const resultSeach = ref([]);
|
||||
|
||||
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) => {
|
||||
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>
|
||||
|
||||
@ -25,12 +25,14 @@ import { useRoute } from "vue-router";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useContactStore } from "@/stores/contactStore";
|
||||
import { useClientLocationStore } from "@/stores/clientLocation";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import ClientDetailPresentation from "@/components/Organism/CRM/ClientDetailPresentation.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const clientStore = useClientStore();
|
||||
const contactStore = useContactStore();
|
||||
const clientLocationStore = useClientLocationStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
// Ensure client_id is a number
|
||||
const client_id = Number(route.params.id);
|
||||
@ -54,15 +56,22 @@ onMounted(async () => {
|
||||
const updateClient = async (data) => {
|
||||
if (!client_id) {
|
||||
console.error("Missing client id");
|
||||
notificationStore.error("Erreur", "ID du client manquant");
|
||||
return;
|
||||
}
|
||||
|
||||
// If data is FormData (e.g. file upload), append the id instead of spreading
|
||||
if (data instanceof FormData) {
|
||||
data.set("id", String(client_id));
|
||||
await clientStore.updateClient(data);
|
||||
} else {
|
||||
await clientStore.updateClient({ id: client_id, ...data });
|
||||
try {
|
||||
// If data is FormData (e.g. file upload), append the id instead of spreading
|
||||
if (data instanceof FormData) {
|
||||
data.set("id", String(client_id));
|
||||
await clientStore.updateClient(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);
|
||||
// Refresh contacts list after creation
|
||||
contacts_client.value = await contactStore.getClientListContact(client_id);
|
||||
notificationStore.created("Contact");
|
||||
} catch (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 {
|
||||
await contactStore.updateContact(modifiedContact);
|
||||
contacts_client.value = await contactStore.getClientListContact(client_id);
|
||||
notificationStore.updated("Contact");
|
||||
} catch (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
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
notificationStore.created("Localisation");
|
||||
} catch (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
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
notificationStore.updated("Localisation");
|
||||
} catch (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
|
||||
const response = await clientLocationStore.getClientLocations(client_id);
|
||||
locations_client.value = response || [];
|
||||
notificationStore.deleted("Localisation");
|
||||
} catch (error) {
|
||||
console.error("Error removing location:", error);
|
||||
notificationStore.error(
|
||||
"Erreur",
|
||||
"Impossible de supprimer la localisation"
|
||||
);
|
||||
}
|
||||
};
|
||||
</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>
|
||||
<fournisseur-detail-presentation
|
||||
v-if="currentFournisseur"
|
||||
:fournisseur="currentFournisseur"
|
||||
:contacts="contacts_fournisseur"
|
||||
v-if="fournisseurStore.currentFournisseur"
|
||||
:fournisseur="fournisseurStore.currentFournisseur"
|
||||
:locations="locations_fournisseur"
|
||||
:is-loading="isLoading"
|
||||
:is-loading="fournisseurStore.isLoading"
|
||||
:fournisseur-avatar="fournisseurAvatar"
|
||||
:active-tab="activeTab"
|
||||
:file-input="fileInput"
|
||||
:contact-loading="contactLoading"
|
||||
:contact-loading="contactStore.isLoading"
|
||||
:location-loading="locationLoading"
|
||||
@update-the-fournisseur="updateFournisseur"
|
||||
@add-new-contact="createNewContact"
|
||||
@updating-contact="updateContact"
|
||||
@contact-removed="handleContactRemoved"
|
||||
@add-new-location="createNewLocation"
|
||||
@modify-location="modifyLocation"
|
||||
@remove-location="removeLocation"
|
||||
@ -22,126 +22,119 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
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";
|
||||
|
||||
const route = useRoute();
|
||||
const fournisseurStore = useFournisseurStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const contactStore = useContactStore();
|
||||
|
||||
// Ensure fournisseur_id is a number
|
||||
const fournisseur_id = Number(route.params.id);
|
||||
const contacts_fournisseur = ref([]);
|
||||
const locations_fournisseur = ref([]);
|
||||
const activeTab = ref("overview");
|
||||
const fournisseurAvatar = ref(null);
|
||||
const fileInput = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const contactLoading = 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 () => {
|
||||
// Dummy contacts data
|
||||
contacts_fournisseur.value = [
|
||||
{
|
||||
id: 1,
|
||||
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,
|
||||
},
|
||||
];
|
||||
if (fournisseur_id) {
|
||||
// Load contacts for this fournisseur
|
||||
await contactStore.fetchContactsByFournisseur(fournisseur_id);
|
||||
await fournisseurStore.fetchFournisseur(fournisseur_id);
|
||||
|
||||
// Dummy locations data
|
||||
locations_fournisseur.value = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
// TODO: Fetch locations when API is ready
|
||||
// locations_fournisseur.value = await locationStore.getFournisseurLocations(fournisseur_id);
|
||||
}
|
||||
});
|
||||
|
||||
const updateFournisseur = async (data) => {
|
||||
console.log("Update fournisseur:", data);
|
||||
// TODO: Implement update logic with store
|
||||
if (!fournisseur_id) {
|
||||
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) => {
|
||||
console.log("Create new contact:", data);
|
||||
// TODO: Implement create contact logic
|
||||
try {
|
||||
// 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) => {
|
||||
console.log("Update contact:", modifiedContact);
|
||||
// TODO: Implement update contact logic
|
||||
try {
|
||||
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) => {
|
||||
console.log("Create new location:", data);
|
||||
// TODO: Implement create location logic
|
||||
try {
|
||||
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) => {
|
||||
console.log("Modify location:", location);
|
||||
// TODO: Implement modify location logic
|
||||
try {
|
||||
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) => {
|
||||
console.log("Remove location:", locationId);
|
||||
// TODO: Implement remove location logic
|
||||
try {
|
||||
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>
|
||||
|
||||
@ -1,104 +1,29 @@
|
||||
<template>
|
||||
<fournisseur-presentation
|
||||
:fournisseur-data="fournisseurs"
|
||||
:loading-data="loading"
|
||||
:fournisseur-data="fournisseurStore.fournisseurs"
|
||||
:loading-data="fournisseurStore.loading"
|
||||
@push-details="goDetails"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
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";
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const fournisseurStore = useFournisseurStore();
|
||||
|
||||
// Dummy data for fournisseurs
|
||||
const fournisseurs = ref([
|
||||
{
|
||||
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,
|
||||
},
|
||||
]);
|
||||
onMounted(async () => {
|
||||
await fournisseurStore.fetchFournisseurs();
|
||||
});
|
||||
|
||||
const goDetails = (id) => {
|
||||
console.log("Navigate to fournisseur details:", id);
|
||||
// router.push({
|
||||
// name: "Fournisseur details",
|
||||
// params: {
|
||||
// id: id,
|
||||
// },
|
||||
// });
|
||||
router.push({
|
||||
name: "Fournisseur details",
|
||||
params: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user