From c5a4fcc546b225e1a9230401c3e53840654126f8 Mon Sep 17 00:00:00 2001 From: Nyavokevin <42602932+nyavokevin@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:00:12 +0300 Subject: [PATCH] add client detail --- .../app/Http/Requests/UpdateClientRequest.php | 2 +- thanasoft-front/.env | 1 + thanasoft-front/NOTIFICATIONS.md | 220 ++++++++ thanasoft-front/NOTIFICATION_QUICK_START.md | 164 ++++++ thanasoft-front/README_NOTIFICATIONS.txt | 89 +++ thanasoft-front/src/App.vue | 5 + .../src/components/NotificationContainer.vue | 122 +++++ .../Organism/CRM/ClientDetailPresentation.vue | 48 ++ .../Organism/CRM/ClientPresentation.vue | 19 +- .../Organism/CRM/ContactPresentation.vue | 10 +- .../molecules/Tables/CRM/ClientTable.vue | 76 ++- .../molecules/Tables/ContactTable.vue | 516 +----------------- .../molecules/client/ClientInformation.vue | 345 ++++++++++++ .../molecules/client/ListContact.vue | 35 ++ .../templates/CRM/ClientDetailTemplate.vue | 14 + .../src/composables/useNotification.ts | 50 ++ .../src/examples/NotificationExamples.vue | 203 +++++++ thanasoft-front/src/router/index.js | 5 + thanasoft-front/src/services/contact.ts | 226 ++++++++ thanasoft-front/src/stores/contactStore.ts | 399 ++++++++++++++ thanasoft-front/src/stores/notification.ts | 79 +++ .../src/views/pages/CRM/ClientDetails.vue | 43 ++ .../src/views/pages/CRM/Clients.vue | 13 + .../src/views/pages/CRM/Contacts.vue | 8 + 24 files changed, 2181 insertions(+), 511 deletions(-) create mode 100644 thanasoft-front/NOTIFICATIONS.md create mode 100644 thanasoft-front/NOTIFICATION_QUICK_START.md create mode 100644 thanasoft-front/README_NOTIFICATIONS.txt create mode 100644 thanasoft-front/src/components/NotificationContainer.vue create mode 100644 thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue create mode 100644 thanasoft-front/src/components/molecules/client/ClientInformation.vue create mode 100644 thanasoft-front/src/components/molecules/client/ListContact.vue create mode 100644 thanasoft-front/src/components/templates/CRM/ClientDetailTemplate.vue create mode 100644 thanasoft-front/src/composables/useNotification.ts create mode 100644 thanasoft-front/src/examples/NotificationExamples.vue create mode 100644 thanasoft-front/src/stores/contactStore.ts create mode 100644 thanasoft-front/src/stores/notification.ts create mode 100644 thanasoft-front/src/views/pages/CRM/ClientDetails.vue diff --git a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php index 1c08317..f44cead 100644 --- a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php @@ -22,7 +22,7 @@ class UpdateClientRequest extends FormRequest public function rules(): array { return [ - 'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre', + 'name' => 'required|string|max:255', 'vat_number' => 'nullable|string|max:32', 'siret' => 'nullable|string|max:20', diff --git a/thanasoft-front/.env b/thanasoft-front/.env index 18d6ec6..f6c84d6 100644 --- a/thanasoft-front/.env +++ b/thanasoft-front/.env @@ -1,3 +1,4 @@ # API base URL for axios (used by src/services/http.ts) # For Laravel Sanctum on local dev (default Laravel port): VUE_APP_API_BASE_URL=http://localhost:8000 +NODE_ENV=production diff --git a/thanasoft-front/NOTIFICATIONS.md b/thanasoft-front/NOTIFICATIONS.md new file mode 100644 index 0000000..e01abe0 --- /dev/null +++ b/thanasoft-front/NOTIFICATIONS.md @@ -0,0 +1,220 @@ +# Système de Notifications + +Un système de notifications toast moderne et réactif pour afficher des messages en haut à droite de l'écran. + +## 📁 Fichiers créés + +- `src/stores/notification.ts` - Store Pinia pour gérer l'état des notifications +- `src/components/NotificationContainer.vue` - Composant conteneur qui affiche les notifications +- `src/composables/useNotification.ts` - Composable pour faciliter l'utilisation +- `src/examples/NotificationExamples.vue` - Exemples d'utilisation + +## 🚀 Utilisation + +### 1. Import dans un composant + +```javascript +import { useNotification } from "@/composables/useNotification"; + +export default { + setup() { + const notification = useNotification(); + + // Votre code... + } +} +``` + +### 2. Notifications CRUD (les plus utilisées) + +```javascript +// Création +notification.created("Le client"); +// Affiche: "Créé avec succès - Le client a été créé avec succès." + +// Mise à jour +notification.updated("La catégorie"); +// Affiche: "Modifié avec succès - La catégorie a été modifié avec succès." + +// Suppression +notification.deleted("Le produit"); +// Affiche: "Supprimé avec succès - Le produit a été supprimé avec succès." +``` + +### 3. Notifications personnalisées + +```javascript +// Succès +notification.success("Titre", "Message de succès"); + +// Erreur +notification.error("Erreur", "Message d'erreur"); + +// Avertissement +notification.warning("Attention", "Message d'avertissement"); + +// Information +notification.info("Info", "Message d'information"); +``` + +### 4. Durée personnalisée + +```javascript +// Par défaut, les notifications durent 5 secondes (5000ms) +// Vous pouvez changer cela : + +notification.created("Le client", 3000); // 3 secondes +notification.success("Titre", "Message", 10000); // 10 secondes +``` + +## 💡 Exemples d'utilisation réels + +### Dans un formulaire de création + +```javascript +const createClient = async () => { + try { + const response = await clientService.create(formData); + notification.created("Le client"); + router.push("/clients"); + } catch (error) { + notification.error("Erreur", "Impossible de créer le client"); + } +}; +``` + +### Dans un formulaire de mise à jour + +```javascript +const updateCategory = async () => { + try { + await categoryService.update(id, formData); + notification.updated("La catégorie"); + } catch (error) { + notification.error("Erreur", error.message); + } +}; +``` + +### Dans une action de suppression + +```javascript +const deleteProduct = async (id) => { + try { + await productService.delete(id); + notification.deleted("Le produit"); + await fetchProducts(); // Recharger la liste + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le produit"); + } +}; +``` + +### Avec SweetAlert2 pour confirmation + +```javascript +const confirmDelete = (id) => { + this.$swal({ + title: "Êtes-vous sûr ?", + text: "Cette action est irréversible !", + icon: "warning", + showCancelButton: true, + confirmButtonText: "Oui, supprimer", + cancelButtonText: "Annuler", + }).then((result) => { + if (result.isConfirmed) { + deleteClient(id); + } + }); +}; + +const deleteClient = async (id) => { + try { + await clientService.delete(id); + notification.deleted("Le client"); + await fetchClients(); + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le client"); + } +}; +``` + +## 🎨 Types de notifications + +| Type | Couleur | Icône | Usage | +|------|---------|-------|-------| +| `success` | Vert | ✓ | Opérations réussies | +| `error` | Rouge | ✗ | Erreurs | +| `warning` | Orange | ⚠ | Avertissements | +| `info` | Bleu | ℹ | Informations | + +## ⚙️ Configuration + +### Position + +Les notifications apparaissent en haut à droite par défaut. Pour changer la position, modifiez le CSS dans `NotificationContainer.vue` : + +```css +.notification-container { + position: fixed; + top: 20px; /* Changer pour bottom: 20px; pour en bas */ + right: 20px; /* Changer pour left: 20px; pour à gauche */ + z-index: 9999; +} +``` + +### Durée par défaut + +Pour changer la durée par défaut (5 secondes), modifiez dans `notification.ts` : + +```typescript +const duration = notification.duration || 5000; // Changer 5000 en valeur désirée +``` + +## 📱 Responsive + +Le système est entièrement responsive. Sur mobile (< 768px), les notifications prennent toute la largeur de l'écran avec une marge de 10px. + +## 🎭 Animations + +Les notifications glissent depuis la droite à l'apparition et disparaissent avec une animation fluide. + +## 🧪 Tester le système + +1. Importez la page d'exemples dans votre router +2. Naviguez vers `/notification-examples` +3. Testez tous les types de notifications + +```javascript +// Dans votre fichier router +{ + path: "/notification-examples", + name: "NotificationExamples", + component: () => import("@/examples/NotificationExamples.vue"), +} +``` + +## 🛠️ Utilisation directe du store (avancé) + +Si vous avez besoin d'un contrôle plus fin : + +```javascript +import { useNotificationStore } from "@/stores/notification"; + +const notificationStore = useNotificationStore(); + +notificationStore.addNotification({ + type: "success", + title: "Titre personnalisé", + message: "Message personnalisé", + duration: 8000 +}); +``` + +## 📝 Notes + +- Les notifications se ferment automatiquement après la durée spécifiée +- Plusieurs notifications peuvent être affichées simultanément +- Chaque notification peut être fermée manuellement en cliquant sur le X +- Les notifications sont gérées globalement via Pinia +- Le composant `NotificationContainer` est déjà intégré dans `App.vue` diff --git a/thanasoft-front/NOTIFICATION_QUICK_START.md b/thanasoft-front/NOTIFICATION_QUICK_START.md new file mode 100644 index 0000000..fffff49 --- /dev/null +++ b/thanasoft-front/NOTIFICATION_QUICK_START.md @@ -0,0 +1,164 @@ +# 🚀 Guide de Démarrage Rapide - Notifications + +## Installation terminée ✅ + +Le système de notifications est déjà intégré dans votre application. Vous pouvez l'utiliser immédiatement ! + +## Utilisation en 3 étapes simples + +### Étape 1 : Importer le composable + +Dans votre composant Vue, ajoutez l'import : + +```javascript +import { useNotification } from "@/composables/useNotification"; +``` + +### Étape 2 : Initialiser dans setup() + +```javascript +export default { + setup() { + const notification = useNotification(); + + return { + notification + }; + } +} +``` + +### Étape 3 : Utiliser dans vos fonctions + +```javascript +// Pour une création +notification.created("Le client"); + +// Pour une mise à jour +notification.updated("La catégorie"); + +// Pour une suppression +notification.deleted("Le produit"); +``` + +## 📋 Exemple complet pour un formulaire de client + +```javascript + +``` + +## 🔄 Pour une mise à jour + +```javascript +const updateClient = async (id, formData) => { + try { + await ClientService.update(id, formData); + notification.updated("Le client"); + } catch (error) { + notification.error("Erreur", error.message); + } +}; +``` + +## ❌ Pour une suppression + +```javascript +const deleteClient = async (id) => { + try { + await ClientService.delete(id); + notification.deleted("Le client"); + await fetchClients(); // Recharger la liste + } catch (error) { + notification.error("Erreur", "Impossible de supprimer le client"); + } +}; +``` + +## 🎨 Autres types de notifications + +```javascript +// Succès personnalisé +notification.success("Bravo !", "L'opération a réussi"); + +// Erreur personnalisée +notification.error("Oups !", "Une erreur s'est produite"); + +// Avertissement +notification.warning("Attention", "Vérifiez vos données"); + +// Information +notification.info("Info", "Nouvelle fonctionnalité disponible"); +``` + +## ⏱️ Changer la durée d'affichage + +Par défaut : 5 secondes. Pour changer : + +```javascript +notification.created("Le client", 3000); // 3 secondes +notification.success("Titre", "Message", 10000); // 10 secondes +``` + +## 📍 Où apparaissent les notifications ? + +- **Position** : En haut à droite de l'écran +- **Mobile** : Pleine largeur en haut +- **Animation** : Glisse depuis la droite +- **Fermeture** : Automatique après 5s ou clic sur X + +## 🧪 Tester le système + +Vous pouvez tester toutes les notifications sur la page d'exemples. +Voir le fichier `NOTIFICATIONS.md` pour plus de détails. + +## 💡 Astuce + +Pour un code plus propre, vous pouvez utiliser le composable directement dans vos méthodes : + +```javascript +const { created, updated, deleted, error, success } = useNotification(); + +// Puis simplement : +created("Le client"); +updated("La catégorie"); +deleted("Le produit"); +``` + +## ❓ Questions fréquentes + +**Q: Les notifications fonctionnent-elles sur toutes les pages ?** +R: Oui, elles sont globales et fonctionnent partout. + +**Q: Puis-je avoir plusieurs notifications en même temps ?** +R: Oui, elles s'empilent automatiquement. + +**Q: Comment changer la position des notifications ?** +R: Voir le fichier `NOTIFICATIONS.md` pour la configuration avancée. + +--- + +📚 **Pour plus d'informations**, consultez `NOTIFICATIONS.md` diff --git a/thanasoft-front/README_NOTIFICATIONS.txt b/thanasoft-front/README_NOTIFICATIONS.txt new file mode 100644 index 0000000..31e7e26 --- /dev/null +++ b/thanasoft-front/README_NOTIFICATIONS.txt @@ -0,0 +1,89 @@ +╔══════════════════════════════════════════════════════════════════════╗ +║ SYSTÈME DE NOTIFICATIONS ║ +║ Installation Complète ✅ ║ +╚══════════════════════════════════════════════════════════════════════╝ + +📁 FICHIERS CRÉÉS: + ├── src/stores/notification.ts + │ └── Store Pinia pour gérer les notifications + │ + ├── src/components/NotificationContainer.vue + │ └── Conteneur qui affiche les notifications en haut + │ + ├── src/composables/useNotification.ts + │ └── Hook pratique pour utiliser les notifications + │ + ├── src/examples/NotificationExamples.vue + │ └── Page de démonstration + │ + ├── NOTIFICATIONS.md + │ └── Documentation complète + │ + └── NOTIFICATION_QUICK_START.md + └── Guide de démarrage rapide + +🔧 INTÉGRATION: + ✅ NotificationContainer ajouté dans App.vue + ✅ Prêt à l'emploi dans tous vos composants + ✅ Responsive mobile & desktop + ✅ Animations fluides + +🚀 UTILISATION RAPIDE: + + 1. Importer dans votre composant: + import { useNotification } from "@/composables/useNotification"; + + 2. Initialiser: + const notification = useNotification(); + + 3. Utiliser: + notification.created("Le client"); + notification.updated("La catégorie"); + notification.deleted("Le produit"); + +📋 EXEMPLES: + + ▸ Création réussie: + notification.created("Le client"); + → Affiche: "Créé avec succès - Le client a été créé avec succès." + + ▸ Mise à jour: + notification.updated("La commande"); + → Affiche: "Modifié avec succès - La commande a été modifié avec succès." + + ▸ Suppression: + notification.deleted("Le produit"); + → Affiche: "Supprimé avec succès - Le produit a été supprimé avec succès." + + ▸ Erreur personnalisée: + notification.error("Erreur", "Impossible de se connecter au serveur"); + + ▸ Succès personnalisé: + notification.success("Parfait !", "L'opération a réussi"); + +🎨 TYPES DISPONIBLES: + ✓ success (vert) - Opérations réussies + ✗ error (rouge) - Erreurs + ⚠ warning (orange)- Avertissements + ℹ info (bleu) - Informations + +📍 POSITION: + • Desktop: En haut à droite + • Mobile: Pleine largeur en haut + • Z-index: 9999 (toujours visible) + +⏱️ DURÉE: + • Par défaut: 5 secondes + • Personnalisable: notification.created("Client", 3000) + • Fermeture manuelle: Clic sur ✕ + +📚 DOCUMENTATION: + → NOTIFICATION_QUICK_START.md : Guide de démarrage (recommandé) + → NOTIFICATIONS.md : Documentation complète + +🧪 TESTER: + Créez une route vers NotificationExamples.vue pour tester! + +═══════════════════════════════════════════════════════════════════════ + +Vous êtes prêt à utiliser les notifications ! 🎉 diff --git a/thanasoft-front/src/App.vue b/thanasoft-front/src/App.vue index d6f8de6..26dc096 100644 --- a/thanasoft-front/src/App.vue +++ b/thanasoft-front/src/App.vue @@ -1,4 +1,7 @@ @@ -20,11 +25,13 @@ import ClientTable from "@/components/molecules/Tables/CRM/ClientTable.vue"; import addButton from "@/components/molecules/new-button/addButton.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue"; -import { defineProps } from "vue"; +import { defineProps, defineEmits } from "vue"; import { useRouter } from "vue-router"; const router = useRouter(); +const emit = defineEmits(["pushDetails"]); + defineProps({ clientData: { type: Array, @@ -41,4 +48,12 @@ const goToClient = () => { name: "Creation client", }); }; + +const goToDetails = (client) => { + emit("pushDetails", client); +}; + +const deleteClient = (client) => { + emit("deleteClient", client); +}; diff --git a/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue index 12c9f9b..ac8610f 100644 --- a/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue +++ b/thanasoft-front/src/components/Organism/CRM/ContactPresentation.vue @@ -10,7 +10,7 @@ @@ -20,4 +20,12 @@ import ContactTable from "@/components/molecules/Tables/ContactTable.vue"; import addButton from "@/components/molecules/new-button/addButton.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue"; +import { defineProps } from "vue"; + +defineProps({ + contacts: { + type: Array, + default: [], + }, +}); diff --git a/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue b/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue index 92e9c98..4f70fd0 100644 --- a/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue +++ b/thanasoft-front/src/components/molecules/Tables/CRM/ClientTable.vue @@ -91,7 +91,6 @@ -
@@ -163,6 +162,33 @@ {{ client.is_active ? "Active" : "Inactive" }}
+ + +
+ + + + + + + + + + +
+ @@ -173,21 +199,25 @@
-
No clients found
+ " +
Aucun client trouvé

- There are no clients to display at the moment. + Aucun client à afficher pour le moment.

+ + diff --git a/thanasoft-front/src/components/molecules/client/ListContact.vue b/thanasoft-front/src/components/molecules/client/ListContact.vue new file mode 100644 index 0000000..d767123 --- /dev/null +++ b/thanasoft-front/src/components/molecules/client/ListContact.vue @@ -0,0 +1,35 @@ + + + diff --git a/thanasoft-front/src/components/templates/CRM/ClientDetailTemplate.vue b/thanasoft-front/src/components/templates/CRM/ClientDetailTemplate.vue new file mode 100644 index 0000000..72d6ae9 --- /dev/null +++ b/thanasoft-front/src/components/templates/CRM/ClientDetailTemplate.vue @@ -0,0 +1,14 @@ + diff --git a/thanasoft-front/src/composables/useNotification.ts b/thanasoft-front/src/composables/useNotification.ts new file mode 100644 index 0000000..5be07fb --- /dev/null +++ b/thanasoft-front/src/composables/useNotification.ts @@ -0,0 +1,50 @@ +import { useNotificationStore } from "@/stores/notification"; + +/** + * Composable pour gérer les notifications dans les composants Vue + * + * Exemple d'utilisation : + * + * const notification = useNotification(); + * + * // Notifications CRUD simples + * notification.created("Le client"); + * notification.updated("La catégorie"); + * notification.deleted("Le produit"); + * + * // Notifications personnalisées + * notification.success("Succès", "Opération réussie"); + * notification.error("Erreur", "Une erreur s'est produite"); + * notification.warning("Attention", "Vérifiez vos données"); + * notification.info("Information", "Nouvelle mise à jour disponible"); + */ +export function useNotification() { + const store = useNotificationStore(); + + return { + // Méthodes de base + success: (title: string, message: string, duration?: number) => + store.success(title, message, duration), + + error: (title: string, message: string, duration?: number) => + store.error(title, message, duration), + + warning: (title: string, message: string, duration?: number) => + store.warning(title, message, duration), + + info: (title: string, message: string, duration?: number) => + store.info(title, message, duration), + + // Méthodes CRUD + created: (entity?: string, duration?: number) => + store.created(entity, duration), + + updated: (entity?: string, duration?: number) => + store.updated(entity, duration), + + deleted: (entity?: string, duration?: number) => + store.deleted(entity, duration), + }; +} + +export default useNotification; diff --git a/thanasoft-front/src/examples/NotificationExamples.vue b/thanasoft-front/src/examples/NotificationExamples.vue new file mode 100644 index 0000000..9cb825d --- /dev/null +++ b/thanasoft-front/src/examples/NotificationExamples.vue @@ -0,0 +1,203 @@ + + + diff --git a/thanasoft-front/src/router/index.js b/thanasoft-front/src/router/index.js index 1b43726..c7ba61b 100644 --- a/thanasoft-front/src/router/index.js +++ b/thanasoft-front/src/router/index.js @@ -385,6 +385,11 @@ const routes = [ name: "Creation client", component: () => import("@/views/pages/CRM/AddClient.vue"), }, + { + path: "/crm/client/:id", + name: "Client details", + component: () => import("@/views/pages/CRM/ClientDetails.vue"), + }, ]; const router = createRouter({ diff --git a/thanasoft-front/src/services/contact.ts b/thanasoft-front/src/services/contact.ts index e69de29..b503987 100644 --- a/thanasoft-front/src/services/contact.ts +++ b/thanasoft-front/src/services/contact.ts @@ -0,0 +1,226 @@ +import { request } from "./http"; + +export interface Contact { + id: number; + first_name: string; + last_name: string; + email: string | null; + phone: string | null; + mobile: string | null; + position: string | null; + notes: string | null; + is_primary: boolean; + created_at: string; + updated_at: string; +} + +export interface ContactListResponse { + data: Contact[]; + meta?: { + current_page: number; + last_page: number; + per_page: number; + total: number; + }; +} + +export interface ContactResponse { + data: Contact; +} + +export interface CreateContactPayload { + first_name: string; + last_name: string; + email?: string | null; + phone?: string | null; + mobile?: string | null; + position?: string | null; + notes?: string | null; + is_primary?: boolean; +} + +export interface UpdateContactPayload extends Partial { + id: number; +} + +export const ContactService = { + /** + * Récupérer tous les contacts avec pagination + */ + async getAllContacts(params?: { + page?: number; + per_page?: number; + search?: string; + is_primary?: boolean; + }): Promise { + const response = await request({ + url: "/api/contacts", + method: "get", + params, + }); + + return response; + }, + + /** + * Récupérer un contact spécifique par ID + */ + async getContact(id: number): Promise { + const response = await request({ + url: `/api/contacts/${id}`, + method: "get", + }); + + return response; + }, + + /** + * Créer un nouveau contact + */ + async createContact(payload: CreateContactPayload): Promise { + const formattedPayload = this.transformContactPayload(payload); + + const response = await request({ + url: "/api/contacts", + method: "post", + data: formattedPayload, + }); + + return response; + }, + + /** + * Mettre à jour un contact existant + */ + async updateContact(payload: UpdateContactPayload): Promise { + const { id, ...updateData } = payload; + const formattedPayload = this.transformContactPayload(updateData); + + const response = await request({ + url: `/api/contacts/${id}`, + method: "put", + data: formattedPayload, + }); + + return response; + }, + + /** + * Supprimer un contact + */ + async deleteContact( + id: number + ): Promise<{ success: boolean; message: string }> { + const response = await request<{ success: boolean; message: string }>({ + url: `/api/contacts/${id}`, + method: "delete", + }); + + return response; + }, + + /** + * Transformer le payload pour correspondre à la structure Laravel + */ + transformContactPayload(payload: Partial): any { + const transformed: any = { ...payload }; + + // Assurer que les valeurs booléennes sont correctement formatées + if (typeof transformed.is_primary === "boolean") { + transformed.is_primary = transformed.is_primary ? 1 : 0; + } + + // Supprimer les valeurs undefined pour éviter de les envoyer + Object.keys(transformed).forEach((key) => { + if (transformed[key] === undefined) { + delete transformed[key]; + } + }); + + return transformed; + }, + + /** + * Rechercher des contacts par nom, email ou autres critères + */ + async searchContacts( + query: string, + params?: { + page?: number; + per_page?: number; + } + ): Promise { + const response = await request({ + url: "/api/contacts", + method: "get", + params: { + search: query, + ...params, + }, + }); + + return response; + }, + + /** + * Obtenir uniquement les contacts principaux + */ + async getPrimaryContacts(params?: { + page?: number; + per_page?: number; + }): Promise { + const response = await request({ + url: "/api/contacts", + method: "get", + params: { + is_primary: true, + ...params, + }, + }); + + return response; + }, + + /** + * Définir un contact comme principal + */ + async setPrimaryContact(id: number): Promise { + const response = await request({ + url: `/api/contacts/${id}/set-primary`, + method: "patch", + data: { + is_primary: true, + }, + }); + + return response; + }, + + /** + * Obtenir le nom complet d'un contact + */ + getFullName(contact: Contact): string { + return `${contact.first_name} ${contact.last_name}`.trim(); + }, + + /** + * Obtenir les contacts par client (si applicable) + */ + async getContactsByClient( + clientId: number, + params?: { + page?: number; + per_page?: number; + } + ): Promise { + const response = await request({ + url: `/api/clients/${clientId}/contacts`, + method: "get", + params, + }); + + return response; + }, +}; + +export default ContactService; \ No newline at end of file diff --git a/thanasoft-front/src/stores/contactStore.ts b/thanasoft-front/src/stores/contactStore.ts new file mode 100644 index 0000000..ca7815c --- /dev/null +++ b/thanasoft-front/src/stores/contactStore.ts @@ -0,0 +1,399 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import ContactService from "@/services/contact"; + +import type { + Contact, + CreateContactPayload, + UpdateContactPayload, + ContactListResponse, +} from "@/services/contact"; + +export const useContactStore = defineStore("contact", () => { + // State + const contacts = ref([]); + const currentContact = ref(null); + const loading = ref(false); + const error = ref(null); + + // Pagination state + const pagination = ref({ + current_page: 1, + last_page: 1, + per_page: 10, + total: 0, + }); + + // Getters + const allContacts = computed(() => contacts.value); + + const primaryContacts = computed(() => + contacts.value.filter((contact) => contact.is_primary) + ); + + const secondaryContacts = computed(() => + contacts.value.filter((contact) => !contact.is_primary) + ); + + const isLoading = computed(() => loading.value); + const hasError = computed(() => error.value !== null); + const getError = computed(() => error.value); + + const getContactById = computed(() => (id: number) => + contacts.value.find((contact) => contact.id === id) + ); + + const getPagination = computed(() => pagination.value); + + const getContactFullName = computed(() => (contact: Contact) => + ContactService.getFullName(contact) + ); + + // Actions + const setLoading = (isLoading: boolean) => { + loading.value = isLoading; + }; + + const setError = (err: string | null) => { + error.value = err; + }; + + const clearError = () => { + error.value = null; + }; + + const setContacts = (newContacts: Contact[]) => { + contacts.value = newContacts; + }; + + const setCurrentContact = (contact: Contact | null) => { + currentContact.value = contact; + }; + + 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, + }; + } + }; + + /** + * Récupérer tous les contacts avec pagination et filtres optionnels + */ + const fetchContacts = async (params?: { + page?: number; + per_page?: number; + search?: string; + is_primary?: boolean; + }) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.getAllContacts(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"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Récupérer un seul contact par ID + */ + const fetchContact = async (id: number) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.getContact(id); + setCurrentContact(response.data); + return response.data; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Échec du chargement du contact"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Créer un nouveau contact + */ + const createContact = async (payload: CreateContactPayload) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.createContact(payload); + // Ajouter le nouveau contact à la liste + contacts.value.push(response.data); + setCurrentContact(response.data); + return response.data; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Échec de la création du contact"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Mettre à jour un contact existant + */ + const updateContact = async (payload: UpdateContactPayload) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.updateContact(payload); + const updatedContact = response.data; + + // Mettre à jour dans la liste des contacts + const index = contacts.value.findIndex( + (contact) => contact.id === updatedContact.id + ); + if (index !== -1) { + contacts.value[index] = updatedContact; + } + + // Mettre à jour le contact actuel s'il s'agit de celui en cours d'édition + if (currentContact.value && currentContact.value.id === updatedContact.id) { + setCurrentContact(updatedContact); + } + + return updatedContact; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Échec de la mise à jour du contact"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Supprimer un contact + */ + const deleteContact = async (id: number) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.deleteContact(id); + + // Retirer de la liste des contacts + contacts.value = contacts.value.filter((contact) => contact.id !== id); + + // Effacer le contact actuel s'il s'agit de celui en cours de suppression + if (currentContact.value && currentContact.value.id === id) { + setCurrentContact(null); + } + + return response; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Échec de la suppression du contact"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Rechercher des contacts + */ + const searchContacts = async ( + query: string, + params?: { + page?: number; + per_page?: number; + } + ) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.searchContacts(query, params); + setContacts(response.data); + if (response.meta) { + setPagination(response.meta); + } + return response; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Échec de la recherche de contacts"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Récupérer uniquement les contacts principaux + */ + const fetchPrimaryContacts = async (params?: { + page?: number; + per_page?: number; + }) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.getPrimaryContacts(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 principaux"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Définir un contact comme principal + */ + const setPrimaryContact = async (id: number) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.setPrimaryContact(id); + const updatedContact = response.data; + + // Mettre à jour dans la liste + const index = contacts.value.findIndex( + (contact) => contact.id === updatedContact.id + ); + if (index !== -1) { + contacts.value[index] = updatedContact; + } + + return updatedContact; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || + err.message || + "Échec de la définition du contact principal"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Récupérer les contacts par client + */ + const fetchContactsByClient = async ( + clientId: number, + params?: { + page?: number; + per_page?: number; + } + ) => { + setLoading(true); + setError(null); + + try { + const response = await ContactService.getContactsByClient(clientId, 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); + } + }; + + /** + * Réinitialiser l'état + */ + const resetState = () => { + contacts.value = []; + currentContact.value = null; + loading.value = false; + error.value = null; + pagination.value = { + current_page: 1, + last_page: 1, + per_page: 10, + total: 0, + }; + }; + + return { + // State + contacts, + currentContact, + loading, + error, + pagination, + + // Getters + allContacts, + primaryContacts, + secondaryContacts, + isLoading, + hasError, + getError, + getContactById, + getPagination, + getContactFullName, + + // Actions + setLoading, + setError, + clearError, + setContacts, + setCurrentContact, + setPagination, + fetchContacts, + fetchContact, + createContact, + updateContact, + deleteContact, + searchContacts, + fetchPrimaryContacts, + setPrimaryContact, + fetchContactsByClient, + resetState, + }; +}); + +export default useContactStore; diff --git a/thanasoft-front/src/stores/notification.ts b/thanasoft-front/src/stores/notification.ts new file mode 100644 index 0000000..467e742 --- /dev/null +++ b/thanasoft-front/src/stores/notification.ts @@ -0,0 +1,79 @@ +import { defineStore } from "pinia"; + +export interface Notification { + id: string; + type: "success" | "error" | "warning" | "info"; + title: string; + message: string; + duration?: number; +} + +export const useNotificationStore = defineStore("notification", { + state: () => ({ + notifications: [] as Notification[], + }), + actions: { + addNotification(notification: Omit) { + const id = Date.now().toString() + Math.random().toString(36); + const duration = notification.duration || 5000; + + const newNotification: Notification = { + ...notification, + id, + duration, + }; + + this.notifications.push(newNotification); + + // Retirer automatiquement la notification après la durée + setTimeout(() => { + this.removeNotification(id); + }, duration); + + return id; + }, + removeNotification(id: string) { + const index = this.notifications.findIndex((n) => n.id === id); + if (index !== -1) { + this.notifications.splice(index, 1); + } + }, + // Méthodes pratiques pour différents types de notifications + success(title: string, message: string, duration?: number) { + return this.addNotification({ type: "success", title, message, duration }); + }, + error(title: string, message: string, duration?: number) { + return this.addNotification({ type: "error", title, message, duration }); + }, + warning(title: string, message: string, duration?: number) { + return this.addNotification({ type: "warning", title, message, duration }); + }, + info(title: string, message: string, duration?: number) { + return this.addNotification({ type: "info", title, message, duration }); + }, + // Méthodes pratiques pour les opérations CRUD + created(entity: string = "Enregistrement", duration?: number) { + return this.success( + "Créé avec succès", + `${entity} a été créé avec succès.`, + duration + ); + }, + updated(entity: string = "Enregistrement", duration?: number) { + return this.success( + "Modifié avec succès", + `${entity} a été modifié avec succès.`, + duration + ); + }, + deleted(entity: string = "Enregistrement", duration?: number) { + return this.success( + "Supprimé avec succès", + `${entity} a été supprimé avec succès.`, + duration + ); + }, + }, +}); + +export default useNotificationStore; diff --git a/thanasoft-front/src/views/pages/CRM/ClientDetails.vue b/thanasoft-front/src/views/pages/CRM/ClientDetails.vue new file mode 100644 index 0000000..4ea5781 --- /dev/null +++ b/thanasoft-front/src/views/pages/CRM/ClientDetails.vue @@ -0,0 +1,43 @@ + + diff --git a/thanasoft-front/src/views/pages/CRM/Clients.vue b/thanasoft-front/src/views/pages/CRM/Clients.vue index 3c304cb..fc26357 100644 --- a/thanasoft-front/src/views/pages/CRM/Clients.vue +++ b/thanasoft-front/src/views/pages/CRM/Clients.vue @@ -2,15 +2,28 @@ diff --git a/thanasoft-front/src/views/pages/CRM/Contacts.vue b/thanasoft-front/src/views/pages/CRM/Contacts.vue index c1c5be5..8a6cb32 100644 --- a/thanasoft-front/src/views/pages/CRM/Contacts.vue +++ b/thanasoft-front/src/views/pages/CRM/Contacts.vue @@ -3,4 +3,12 @@