add client detail
This commit is contained in:
parent
175446adbe
commit
c5a4fcc546
@ -22,7 +22,7 @@ class UpdateClientRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre',
|
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'vat_number' => 'nullable|string|max:32',
|
'vat_number' => 'nullable|string|max:32',
|
||||||
'siret' => 'nullable|string|max:20',
|
'siret' => 'nullable|string|max:20',
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
# API base URL for axios (used by src/services/http.ts)
|
# API base URL for axios (used by src/services/http.ts)
|
||||||
# For Laravel Sanctum on local dev (default Laravel port):
|
# For Laravel Sanctum on local dev (default Laravel port):
|
||||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||||
|
NODE_ENV=production
|
||||||
|
|||||||
220
thanasoft-front/NOTIFICATIONS.md
Normal file
220
thanasoft-front/NOTIFICATIONS.md
Normal file
@ -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`
|
||||||
164
thanasoft-front/NOTIFICATION_QUICK_START.md
Normal file
164
thanasoft-front/NOTIFICATION_QUICK_START.md
Normal file
@ -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
|
||||||
|
<script>
|
||||||
|
import { useNotification } from "@/composables/useNotification";
|
||||||
|
import ClientService from "@/services/client";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NewClientForm",
|
||||||
|
setup() {
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const createClient = async (formData) => {
|
||||||
|
try {
|
||||||
|
await ClientService.create(formData);
|
||||||
|
notification.created("Le client");
|
||||||
|
// Redirection ou autre action...
|
||||||
|
} catch (error) {
|
||||||
|
notification.error("Erreur", "Impossible de créer le client");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createClient
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 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`
|
||||||
89
thanasoft-front/README_NOTIFICATIONS.txt
Normal file
89
thanasoft-front/README_NOTIFICATIONS.txt
Normal file
@ -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 ! 🎉
|
||||||
@ -1,4 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- Conteneur de notifications global -->
|
||||||
|
<notification-container />
|
||||||
|
|
||||||
<!-- Guest Layout: for login, signup, auth pages -->
|
<!-- Guest Layout: for login, signup, auth pages -->
|
||||||
<div v-if="isGuestRoute" class="guest-layout">
|
<div v-if="isGuestRoute" class="guest-layout">
|
||||||
<router-view />
|
<router-view />
|
||||||
@ -35,6 +38,7 @@ import Sidenav from "./examples/Sidenav";
|
|||||||
import Configurator from "@/examples/Configurator.vue";
|
import Configurator from "@/examples/Configurator.vue";
|
||||||
import Navbar from "@/examples/Navbars/Navbar.vue";
|
import Navbar from "@/examples/Navbars/Navbar.vue";
|
||||||
import AppFooter from "@/examples/Footer.vue";
|
import AppFooter from "@/examples/Footer.vue";
|
||||||
|
import NotificationContainer from "@/components/NotificationContainer.vue";
|
||||||
import { mapMutations, mapState } from "vuex";
|
import { mapMutations, mapState } from "vuex";
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
@ -43,6 +47,7 @@ export default {
|
|||||||
Configurator,
|
Configurator,
|
||||||
Navbar,
|
Navbar,
|
||||||
AppFooter,
|
AppFooter,
|
||||||
|
NotificationContainer,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
|
|||||||
122
thanasoft-front/src/components/NotificationContainer.vue
Normal file
122
thanasoft-front/src/components/NotificationContainer.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notification-container">
|
||||||
|
<transition-group name="notification-slide">
|
||||||
|
<div
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="notification-item"
|
||||||
|
>
|
||||||
|
<soft-snackbar
|
||||||
|
:title="notification.title"
|
||||||
|
:description="notification.message"
|
||||||
|
:icon="getIcon(notification.type)"
|
||||||
|
:color="getColor(notification.type)"
|
||||||
|
:close-handler="() => removeNotification(notification.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import SoftSnackbar from "@/components/SoftSnackbar.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NotificationContainer",
|
||||||
|
components: {
|
||||||
|
SoftSnackbar,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const getIcon = (type) => {
|
||||||
|
const icons = {
|
||||||
|
success: { component: "ni ni-check-bold", color: "success" },
|
||||||
|
error: { component: "ni ni-fat-remove", color: "danger" },
|
||||||
|
warning: { component: "ni ni-bell-55", color: "warning" },
|
||||||
|
info: { component: "ni ni-bulb-61", color: "info" },
|
||||||
|
};
|
||||||
|
return icons[type] || icons.info;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
success: "white",
|
||||||
|
error: "white",
|
||||||
|
warning: "white",
|
||||||
|
info: "white",
|
||||||
|
};
|
||||||
|
return colors[type] || "white";
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNotification = (id) => {
|
||||||
|
notificationStore.removeNotification(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications: notificationStore.notifications,
|
||||||
|
getIcon,
|
||||||
|
getColor,
|
||||||
|
removeNotification,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
pointer-events: auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation de transition */
|
||||||
|
.notification-slide-enter-active {
|
||||||
|
animation: slideInRight 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-slide-leave-active {
|
||||||
|
animation: slideOutRight 0.4s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive pour mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notification-container {
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<client-detail-template>
|
||||||
|
<template #client-information>
|
||||||
|
<client-information
|
||||||
|
:client_id="client.id"
|
||||||
|
:name="client.name"
|
||||||
|
:categorie="client.categorie"
|
||||||
|
:siret="client.siret"
|
||||||
|
:vat_number="client.vat_number"
|
||||||
|
:phone="client.phone"
|
||||||
|
:email="client.email"
|
||||||
|
:billing_address_line1="client.billing_address_line1"
|
||||||
|
:billing_postal_code="client.billing_postal_code"
|
||||||
|
:billing_city="client.billing_city"
|
||||||
|
:billing_country_code="client.billing_country_code"
|
||||||
|
:notes="client.notes"
|
||||||
|
:is_active="client.is_active"
|
||||||
|
:action="{
|
||||||
|
route: `/clients/${client.id}/edit`,
|
||||||
|
tooltip: 'Modifier les informations'
|
||||||
|
}"
|
||||||
|
@update:client="handleUpdateClient"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #contact-list>
|
||||||
|
<list-contact />
|
||||||
|
</template>
|
||||||
|
</client-detail-template>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
|
||||||
|
import ClientInformation from "@/components/molecules/client/ClientInformation.vue";
|
||||||
|
import ListContact from "@/components/molecules/client/ListContact.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
client: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:client"]);
|
||||||
|
|
||||||
|
const handleUpdateClient = (updateData) => {
|
||||||
|
emit("update:client", updateData);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -10,7 +10,12 @@
|
|||||||
<table-action />
|
<table-action />
|
||||||
</template>
|
</template>
|
||||||
<template #client-table>
|
<template #client-table>
|
||||||
<client-table :data="clientData" :loading="loadingData" />
|
<client-table
|
||||||
|
:data="clientData"
|
||||||
|
:loading="loadingData"
|
||||||
|
@view="goToDetails"
|
||||||
|
@delete="deleteClient"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</client-template>
|
</client-template>
|
||||||
</template>
|
</template>
|
||||||
@ -20,11 +25,13 @@ import ClientTable from "@/components/molecules/Tables/CRM/ClientTable.vue";
|
|||||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
import { defineProps } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const emit = defineEmits(["pushDetails"]);
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
clientData: {
|
clientData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -41,4 +48,12 @@ const goToClient = () => {
|
|||||||
name: "Creation client",
|
name: "Creation client",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToDetails = (client) => {
|
||||||
|
emit("pushDetails", client);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteClient = (client) => {
|
||||||
|
emit("deleteClient", client);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<table-action />
|
<table-action />
|
||||||
</template>
|
</template>
|
||||||
<template #contact-table>
|
<template #contact-table>
|
||||||
<contact-table />
|
<contact-table :contacts-data="contacts" />
|
||||||
</template>
|
</template>
|
||||||
</contact-template>
|
</contact-template>
|
||||||
</template>
|
</template>
|
||||||
@ -20,4 +20,12 @@ import ContactTable from "@/components/molecules/Tables/ContactTable.vue";
|
|||||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
contacts: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -91,7 +91,6 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="client in data" :key="client.id">
|
<tr v-for="client in data" :key="client.id">
|
||||||
<!-- Commercial Column -->
|
<!-- Commercial Column -->
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-checkbox />
|
<soft-checkbox />
|
||||||
@ -163,6 +162,33 @@
|
|||||||
<span>{{ client.is_active ? "Active" : "Inactive" }}</span>
|
<span>{{ client.is_active ? "Active" : "Inactive" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<!-- View Button -->
|
||||||
|
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="View Client"
|
||||||
|
:data-client-id="client.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
title="Delete Client"
|
||||||
|
:data-client-id="client.id"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -173,21 +199,25 @@
|
|||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<i class="fas fa-users fa-3x text-muted"></i>
|
<i class="fas fa-users fa-3x text-muted"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="empty-title">No clients found</h5>
|
"
|
||||||
|
<h5 class="empty-title">Aucun client trouvé</h5>
|
||||||
<p class="empty-text text-muted">
|
<p class="empty-text text-muted">
|
||||||
There are no clients to display at the moment.
|
Aucun client à afficher pour le moment.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
import { DataTable } from "simple-datatables";
|
import { DataTable } from "simple-datatables";
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
import { defineProps } from "vue";
|
import addButton from "../../new-button/addButton.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view"]);
|
||||||
|
|
||||||
// Sample avatar images
|
// Sample avatar images
|
||||||
import img1 from "@/assets/img/team-2.jpg";
|
import img1 from "@/assets/img/team-2.jpg";
|
||||||
@ -252,6 +282,7 @@ const initializeDataTable = () => {
|
|||||||
// Destroy existing instance if it exists
|
// Destroy existing instance if it exists
|
||||||
if (dataTableInstance.value) {
|
if (dataTableInstance.value) {
|
||||||
dataTableInstance.value.destroy();
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataTableEl = document.getElementById("contact-list");
|
const dataTableEl = document.getElementById("contact-list");
|
||||||
@ -262,6 +293,22 @@ const initializeDataTable = () => {
|
|||||||
perPage: 10,
|
perPage: 10,
|
||||||
perPageSelect: [5, 10, 15, 20],
|
perPageSelect: [5, 10, 15, 20],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
const clientId = button.getAttribute("data-client-id");
|
||||||
|
if (button.title === "Delete Client" || button.querySelector(".fa-trash")) {
|
||||||
|
emit("delete", clientId);
|
||||||
|
} else if (
|
||||||
|
button.title === "View Client" ||
|
||||||
|
button.textContent?.includes("Test")
|
||||||
|
) {
|
||||||
|
emit("view", clientId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -279,6 +326,16 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const dataTableEl = document.getElementById("contact-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize data
|
// Initialize data
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.loading && props.data.length > 0) {
|
if (!props.loading && props.data.length > 0) {
|
||||||
@ -425,4 +482,13 @@ onMounted(() => {
|
|||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-icon.small {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,516 +3,28 @@
|
|||||||
<table id="order-list" class="table table-flush">
|
<table id="order-list" class="table table-flush">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
<th>Contact</th>
|
||||||
<th>Date de creation</th>
|
<th>Email</th>
|
||||||
<th>Nom</th>
|
<th>Phone</th>
|
||||||
<th>Customer</th>
|
<th>Date Creation</th>
|
||||||
<th>Product</th>
|
<th>Actions</th>
|
||||||
<th>Revenue</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody></tbody>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10421</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 10:20 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img1"
|
|
||||||
class="me-2"
|
|
||||||
size="xs"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Orlando Imieto</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Nike Sport V2</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$140,20</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10422</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 10:53 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img2"
|
|
||||||
size="xs"
|
|
||||||
class="me-2"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Alice Murinho</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Valvet T-shirt</span>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$42,00</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10423</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 11:13 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="dark"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-undo" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Refunded</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-dark">
|
|
||||||
<span>M</span>
|
|
||||||
</div>
|
|
||||||
<span>Michael Mirra</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">
|
|
||||||
Leather Wallet
|
|
||||||
<span class="text-secondary ms-2">+1 more</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$25,50</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10424</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 12:20 PM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img3"
|
|
||||||
size="xs"
|
|
||||||
class="me-2"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Andrew Nichel</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Bracelet Onu-Lino</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$19,40</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10425</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 1:40 PM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="danger"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Canceled</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img4"
|
|
||||||
size="xs"
|
|
||||||
class="me-2"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Sebastian Koga</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">
|
|
||||||
Phone Case Pink
|
|
||||||
<span class="text-secondary ms-2">x 2</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$44,90</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10426</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 2:19 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-success">
|
|
||||||
<span>L</span>
|
|
||||||
</div>
|
|
||||||
<span>Laur Gilbert</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Backpack Niver</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$112,50</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10427</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">1 Nov, 3:42 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-dark">
|
|
||||||
<span>I</span>
|
|
||||||
</div>
|
|
||||||
<span>Iryna Innda</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Adidas Vio</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$200,00</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10428</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">2 Nov, 9:32 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-dark">
|
|
||||||
<span>A</span>
|
|
||||||
</div>
|
|
||||||
<span>Arrias Liunda</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Airpods 2 Gen</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$350,00</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10429</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">2 Nov, 10:14 AM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img5"
|
|
||||||
size="xs"
|
|
||||||
class="me-2"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Rugna Ilpio</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Bracelet Warret</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$15,00</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10430</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">2 Nov, 12:56 PM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="dark"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-undo" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Refunded</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-avatar
|
|
||||||
:img="img6"
|
|
||||||
size="xs"
|
|
||||||
class="me-2"
|
|
||||||
alt="user image"
|
|
||||||
circular
|
|
||||||
/>
|
|
||||||
<span>Anna Landa</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">
|
|
||||||
Watter Bottle India
|
|
||||||
<span class="text-secondary ms-2">x 3</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$25,00</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10431</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">2 Nov, 3:12 PM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-dark">
|
|
||||||
<span>K</span>
|
|
||||||
</div>
|
|
||||||
<span>Karl Innas</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Kitchen Gadgets</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$164,90</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">#10432</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">2 Nov, 5:12 PM</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="success"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>Paid</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="avatar avatar-xs me-2 bg-gradient-info">
|
|
||||||
<span>O</span>
|
|
||||||
</div>
|
|
||||||
<span>Oana Kilas</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">Office Papers</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="my-2 text-xs">$23,90</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { DataTable } from "simple-datatables";
|
import { DataTable } from "simple-datatables";
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
import { defineProps } from "vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
|
||||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
defineProps({
|
||||||
import img1 from "@/assets/img/team-2.jpg";
|
contacts: {
|
||||||
import img2 from "@/assets/img/team-1.jpg";
|
type: Array,
|
||||||
import img3 from "@/assets/img/team-3.jpg";
|
default: [],
|
||||||
import img4 from "@/assets/img/team-4.jpg";
|
},
|
||||||
import img5 from "@/assets/img/team-5.jpg";
|
});
|
||||||
import img6 from "@/assets/img/ivana-squares.jpg";
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const dataTableEl = document.getElementById("order-list");
|
const dataTableEl = document.getElementById("order-list");
|
||||||
|
|||||||
@ -0,0 +1,345 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="p-3 pb-0 card-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 d-flex align-items-center">
|
||||||
|
<h6 class="mb-0">{{ title }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<i
|
||||||
|
v-if="!isEditing"
|
||||||
|
@click="toggleEditMode"
|
||||||
|
class="text-sm fas fa-user-edit text-secondary cursor-pointer"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
:title="action.tooltip"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
></i>
|
||||||
|
<div v-else>
|
||||||
|
<i
|
||||||
|
@click="saveChanges"
|
||||||
|
class="text-sm fas fa-save text-success cursor-pointer me-2"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
title="Sauvegarder"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
@click="cancelEdit"
|
||||||
|
class="text-sm fas fa-times text-danger cursor-pointer"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
title="Annuler"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 card-body">
|
||||||
|
<hr class="my-1 horizontal gray-light" />
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="pt-0 text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Nom du client:</strong>
|
||||||
|
<span v-if="!isEditing">{{ name }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editForm.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm d-inline-block w-auto"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Catégorie:</strong>
|
||||||
|
<span v-if="!isEditing" :class="`badge badge-${getCategoryColor(categorie)}`">
|
||||||
|
{{ getCategoryLabel(categorie) }}
|
||||||
|
</span>
|
||||||
|
<select v-else v-model="editForm.categorie" class="form-select form-select-sm d-inline-block w-auto">
|
||||||
|
<option value="entreprise">Entreprise</option>
|
||||||
|
<option value="particulier">Particulier</option>
|
||||||
|
<option value="association">Association</option>
|
||||||
|
<option value="administration">Administration</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">SIRET:</strong>
|
||||||
|
<span v-if="!isEditing">{{ siret }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editForm.siret"
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm d-inline-block w-auto"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">TVA:</strong>
|
||||||
|
<span v-if="!isEditing">{{ vat_number }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editForm.vat_number"
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm d-inline-block w-auto"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Téléphone:</strong>
|
||||||
|
<span v-if="!isEditing">{{ phone }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editForm.phone"
|
||||||
|
type="text"
|
||||||
|
class="form-control form-control-sm d-inline-block w-auto"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Email:</strong>
|
||||||
|
<span v-if="!isEditing">{{ email }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editForm.email"
|
||||||
|
type="email"
|
||||||
|
class="form-control form-control-sm d-inline-block w-auto"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Adresse:</strong>
|
||||||
|
<span v-if="!isEditing">
|
||||||
|
{{ billing_address_line1 }}, {{ billing_postal_code }}
|
||||||
|
{{ billing_city }}, {{ getCountryName(billing_country_code) }}
|
||||||
|
</span>
|
||||||
|
<div v-else class="d-inline-block">
|
||||||
|
<input
|
||||||
|
v-model="editForm.billing_address_line1"
|
||||||
|
type="text"
|
||||||
|
placeholder="Adresse"
|
||||||
|
class="form-control form-control-sm mb-1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="editForm.billing_postal_code"
|
||||||
|
type="text"
|
||||||
|
placeholder="Code postal"
|
||||||
|
class="form-control form-control-sm mb-1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="editForm.billing_city"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ville"
|
||||||
|
class="form-control form-control-sm mb-1"
|
||||||
|
/>
|
||||||
|
<select v-model="editForm.billing_country_code" class="form-select form-select-sm">
|
||||||
|
<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="UK">Royaume-Uni</option>
|
||||||
|
<option value="US">États-Unis</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Statut:</strong>
|
||||||
|
<span v-if="!isEditing" :class="`badge badge-${is_active ? 'success' : 'danger'}`">
|
||||||
|
{{ is_active ? "Actif" : "Inactif" }}
|
||||||
|
</span>
|
||||||
|
<select v-else v-model="editForm.is_active" class="form-select form-select-sm d-inline-block w-auto">
|
||||||
|
<option :value="true">Actif</option>
|
||||||
|
<option :value="false">Inactif</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="text-sm border-0 list-group-item ps-0">
|
||||||
|
<strong class="text-dark">Notes:</strong>
|
||||||
|
<span v-if="!isEditing">{{ notes || 'Aucune note' }}</span>
|
||||||
|
<textarea
|
||||||
|
v-else
|
||||||
|
v-model="editForm.notes"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, ref, reactive } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
client_id: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "Informations du Client",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "Détails complets des informations client",
|
||||||
|
},
|
||||||
|
// Client data props
|
||||||
|
categorie: {
|
||||||
|
type: String,
|
||||||
|
default: "entreprise",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: "Entreprise test",
|
||||||
|
},
|
||||||
|
vat_number: {
|
||||||
|
type: String,
|
||||||
|
default: "FR98765432109",
|
||||||
|
},
|
||||||
|
siret: {
|
||||||
|
type: String,
|
||||||
|
default: "98765432100019",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
default: "compta@technoplus.fr",
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: String,
|
||||||
|
default: "+33198765432",
|
||||||
|
},
|
||||||
|
billing_address_line1: {
|
||||||
|
type: String,
|
||||||
|
default: "789 Boulevard Haussmann",
|
||||||
|
},
|
||||||
|
billing_postal_code: {
|
||||||
|
type: String,
|
||||||
|
default: "75009",
|
||||||
|
},
|
||||||
|
billing_city: {
|
||||||
|
type: String,
|
||||||
|
default: "Paris",
|
||||||
|
},
|
||||||
|
billing_country_code: {
|
||||||
|
type: String,
|
||||||
|
default: "FR",
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: String,
|
||||||
|
default: "Nouveau client entreprise",
|
||||||
|
},
|
||||||
|
is_active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
route: "javascript:;",
|
||||||
|
tooltip: "Modifier les informations",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:client"]);
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const editForm = reactive({
|
||||||
|
name: props.name,
|
||||||
|
categorie: props.categorie,
|
||||||
|
siret: props.siret,
|
||||||
|
vat_number: props.vat_number,
|
||||||
|
phone: props.phone,
|
||||||
|
email: props.email,
|
||||||
|
billing_address_line1: props.billing_address_line1,
|
||||||
|
billing_postal_code: props.billing_postal_code,
|
||||||
|
billing_city: props.billing_city,
|
||||||
|
billing_country_code: props.billing_country_code,
|
||||||
|
notes: props.notes,
|
||||||
|
is_active: props.is_active,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit methods
|
||||||
|
const toggleEditMode = () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
// Reset form with current props
|
||||||
|
editForm.name = props.name;
|
||||||
|
editForm.categorie = props.categorie;
|
||||||
|
editForm.siret = props.siret;
|
||||||
|
editForm.vat_number = props.vat_number;
|
||||||
|
editForm.phone = props.phone;
|
||||||
|
editForm.email = props.email;
|
||||||
|
editForm.billing_address_line1 = props.billing_address_line1;
|
||||||
|
editForm.billing_postal_code = props.billing_postal_code;
|
||||||
|
editForm.billing_city = props.billing_city;
|
||||||
|
editForm.billing_country_code = props.billing_country_code;
|
||||||
|
editForm.notes = props.notes;
|
||||||
|
editForm.is_active = props.is_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
const updateData = {
|
||||||
|
id: props.client_id,
|
||||||
|
name: editForm.name,
|
||||||
|
categorie: editForm.categorie,
|
||||||
|
siret: editForm.siret,
|
||||||
|
vat_number: editForm.vat_number,
|
||||||
|
phone: editForm.phone,
|
||||||
|
email: editForm.email,
|
||||||
|
billing_address_line1: editForm.billing_address_line1,
|
||||||
|
billing_postal_code: editForm.billing_postal_code,
|
||||||
|
billing_city: editForm.billing_city,
|
||||||
|
billing_country_code: editForm.billing_country_code,
|
||||||
|
notes: editForm.notes,
|
||||||
|
is_active: editForm.is_active,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit("update:client", updateData);
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getCategoryColor = (category) => {
|
||||||
|
const colors = {
|
||||||
|
entreprise: "primary",
|
||||||
|
particulier: "success",
|
||||||
|
association: "warning",
|
||||||
|
administration: "info",
|
||||||
|
};
|
||||||
|
return colors[category] || "secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryLabel = (category) => {
|
||||||
|
const labels = {
|
||||||
|
entreprise: "Entreprise",
|
||||||
|
particulier: "Particulier",
|
||||||
|
association: "Association",
|
||||||
|
administration: "Administration",
|
||||||
|
};
|
||||||
|
return labels[category] || category;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCountryName = (countryCode) => {
|
||||||
|
const countries = {
|
||||||
|
FR: "France",
|
||||||
|
BE: "Belgique",
|
||||||
|
CH: "Suisse",
|
||||||
|
LU: "Luxembourg",
|
||||||
|
DE: "Allemagne",
|
||||||
|
ES: "Espagne",
|
||||||
|
IT: "Italie",
|
||||||
|
UK: "Royaume-Uni",
|
||||||
|
US: "États-Unis",
|
||||||
|
};
|
||||||
|
return countries[countryCode] || countryCode;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.badge {
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding: 0.35em 0.65em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="p-3 pb-0 card-header">
|
||||||
|
<h6 class="mb-0">Contacts</h6>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 card-body border-radius-lg">
|
||||||
|
<div
|
||||||
|
v-for="contact of contacts"
|
||||||
|
:key="contact.id"
|
||||||
|
class="d-flex"
|
||||||
|
:class="index !== 0 ? 'mt-4' : ''"
|
||||||
|
>
|
||||||
|
<div class="ms-3">
|
||||||
|
<div class="numbers">
|
||||||
|
<h6 class="mb-1 text-sm text-dark">
|
||||||
|
{{ contact.first_name + " " + contact.first_name }}
|
||||||
|
</h6>
|
||||||
|
<span class="text-sm">{{ email }}</span>
|
||||||
|
<span class="text-sm">{{ phone }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
defineProps({
|
||||||
|
contacts: {
|
||||||
|
type: Object,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="py-4 container fluid">
|
||||||
|
<div class="mt-3 row">
|
||||||
|
<div class="col-12 col-md-6 col-xl-4 mt-md-0">
|
||||||
|
<slot name="client-information" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3 col-xl-r mt-md-0">
|
||||||
|
<slot name="contact-list" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
50
thanasoft-front/src/composables/useNotification.ts
Normal file
50
thanasoft-front/src/composables/useNotification.ts
Normal file
@ -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;
|
||||||
203
thanasoft-front/src/examples/NotificationExamples.vue
Normal file
203
thanasoft-front/src/examples/NotificationExamples.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h5>Exemples de Notifications</h5>
|
||||||
|
<p class="text-sm">
|
||||||
|
Testez le système de notifications avec différents types
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Notifications CRUD -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Notifications CRUD</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-success w-100"
|
||||||
|
@click="showCreatedNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-fat-add me-2"></i>
|
||||||
|
Création
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-info w-100"
|
||||||
|
@click="showUpdatedNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-settings me-2"></i>
|
||||||
|
Mise à jour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-danger w-100"
|
||||||
|
@click="showDeletedNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-fat-delete me-2"></i>
|
||||||
|
Suppression
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notifications par type -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Notifications par Type</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn bg-gradient-success w-100"
|
||||||
|
@click="showSuccessNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-check-bold me-2"></i>
|
||||||
|
Succès
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn bg-gradient-danger w-100"
|
||||||
|
@click="showErrorNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-fat-remove me-2"></i>
|
||||||
|
Erreur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn bg-gradient-warning w-100"
|
||||||
|
@click="showWarningNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-bell-55 me-2"></i>
|
||||||
|
Attention
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn bg-gradient-info w-100"
|
||||||
|
@click="showInfoNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-bulb-61 me-2"></i>
|
||||||
|
Information
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notifications multiples -->
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-3">Tests Avancés</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary w-100"
|
||||||
|
@click="showMultipleNotifications"
|
||||||
|
>
|
||||||
|
<i class="ni ni-bullet-list-67 me-2"></i>
|
||||||
|
Afficher plusieurs notifications
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary w-100"
|
||||||
|
@click="showLongDurationNotification"
|
||||||
|
>
|
||||||
|
<i class="ni ni-time-alarm me-2"></i>
|
||||||
|
Notification longue durée (10s)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useNotification } from "@/composables/useNotification";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NotificationExamples",
|
||||||
|
setup() {
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
// Notifications CRUD
|
||||||
|
const showCreatedNotification = () => {
|
||||||
|
notification.created("Le client");
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUpdatedNotification = () => {
|
||||||
|
notification.updated("La catégorie");
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDeletedNotification = () => {
|
||||||
|
notification.deleted("Le produit");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notifications par type
|
||||||
|
const showSuccessNotification = () => {
|
||||||
|
notification.success(
|
||||||
|
"Opération réussie",
|
||||||
|
"L'action a été effectuée avec succès."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showErrorNotification = () => {
|
||||||
|
notification.error(
|
||||||
|
"Erreur rencontrée",
|
||||||
|
"Une erreur s'est produite lors du traitement."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showWarningNotification = () => {
|
||||||
|
notification.warning(
|
||||||
|
"Attention requise",
|
||||||
|
"Veuillez vérifier les données avant de continuer."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInfoNotification = () => {
|
||||||
|
notification.info(
|
||||||
|
"Information",
|
||||||
|
"Une nouvelle mise à jour est disponible."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tests avancés
|
||||||
|
const showMultipleNotifications = () => {
|
||||||
|
notification.success("Notification 1", "Première notification");
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.info("Notification 2", "Deuxième notification");
|
||||||
|
}, 500);
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.warning("Notification 3", "Troisième notification");
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLongDurationNotification = () => {
|
||||||
|
notification.info(
|
||||||
|
"Notification longue",
|
||||||
|
"Cette notification restera visible pendant 10 secondes.",
|
||||||
|
10000
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showCreatedNotification,
|
||||||
|
showUpdatedNotification,
|
||||||
|
showDeletedNotification,
|
||||||
|
showSuccessNotification,
|
||||||
|
showErrorNotification,
|
||||||
|
showWarningNotification,
|
||||||
|
showInfoNotification,
|
||||||
|
showMultipleNotifications,
|
||||||
|
showLongDurationNotification,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -385,6 +385,11 @@ const routes = [
|
|||||||
name: "Creation client",
|
name: "Creation client",
|
||||||
component: () => import("@/views/pages/CRM/AddClient.vue"),
|
component: () => import("@/views/pages/CRM/AddClient.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/crm/client/:id",
|
||||||
|
name: "Client details",
|
||||||
|
component: () => import("@/views/pages/CRM/ClientDetails.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@ -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<CreateContactPayload> {
|
||||||
|
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<ContactListResponse> {
|
||||||
|
const response = await request<ContactListResponse>({
|
||||||
|
url: "/api/contacts",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer un contact spécifique par ID
|
||||||
|
*/
|
||||||
|
async getContact(id: number): Promise<ContactResponse> {
|
||||||
|
const response = await request<ContactResponse>({
|
||||||
|
url: `/api/contacts/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Créer un nouveau contact
|
||||||
|
*/
|
||||||
|
async createContact(payload: CreateContactPayload): Promise<ContactResponse> {
|
||||||
|
const formattedPayload = this.transformContactPayload(payload);
|
||||||
|
|
||||||
|
const response = await request<ContactResponse>({
|
||||||
|
url: "/api/contacts",
|
||||||
|
method: "post",
|
||||||
|
data: formattedPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour un contact existant
|
||||||
|
*/
|
||||||
|
async updateContact(payload: UpdateContactPayload): Promise<ContactResponse> {
|
||||||
|
const { id, ...updateData } = payload;
|
||||||
|
const formattedPayload = this.transformContactPayload(updateData);
|
||||||
|
|
||||||
|
const response = await request<ContactResponse>({
|
||||||
|
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<CreateContactPayload>): 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<ContactListResponse> {
|
||||||
|
const response = await request<ContactListResponse>({
|
||||||
|
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<ContactListResponse> {
|
||||||
|
const response = await request<ContactListResponse>({
|
||||||
|
url: "/api/contacts",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
is_primary: true,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définir un contact comme principal
|
||||||
|
*/
|
||||||
|
async setPrimaryContact(id: number): Promise<ContactResponse> {
|
||||||
|
const response = await request<ContactResponse>({
|
||||||
|
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<ContactListResponse> {
|
||||||
|
const response = await request<ContactListResponse>({
|
||||||
|
url: `/api/clients/${clientId}/contacts`,
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactService;
|
||||||
399
thanasoft-front/src/stores/contactStore.ts
Normal file
399
thanasoft-front/src/stores/contactStore.ts
Normal file
@ -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<Contact[]>([]);
|
||||||
|
const currentContact = ref<Contact | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(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;
|
||||||
79
thanasoft-front/src/stores/notification.ts
Normal file
79
thanasoft-front/src/stores/notification.ts
Normal file
@ -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<Notification, "id">) {
|
||||||
|
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;
|
||||||
43
thanasoft-front/src/views/pages/CRM/ClientDetails.vue
Normal file
43
thanasoft-front/src/views/pages/CRM/ClientDetails.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<client-detail-presentation
|
||||||
|
v-if="clientStore.currentClient"
|
||||||
|
:client="clientStore.currentClient"
|
||||||
|
@update:client="handleUpdateClient"
|
||||||
|
/>
|
||||||
|
<div v-else-if="clientStore.isLoading" class="text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="clientStore.hasError" class="alert alert-danger m-3">
|
||||||
|
{{ clientStore.getError }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import ClientDetailPresentation from "@/components/Organism/CRM/ClientDetailPresentation.vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const clientStore = useClientStore();
|
||||||
|
|
||||||
|
const client_id = route.params.id;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (client_id) {
|
||||||
|
await clientStore.fetchClient(Number(client_id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateClient = async (updateData) => {
|
||||||
|
try {
|
||||||
|
await clientStore.updateClient(updateData);
|
||||||
|
// Optionally show a success message
|
||||||
|
console.log("Client mis à jour avec succès");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la mise à jour du client:", error);
|
||||||
|
// Optionally show an error message to the user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -2,15 +2,28 @@
|
|||||||
<client-presentation
|
<client-presentation
|
||||||
:client-data="clientStore.clients"
|
:client-data="clientStore.clients"
|
||||||
:loading-data="clientStore.loading"
|
:loading-data="clientStore.loading"
|
||||||
|
@push-details="goDetails"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ClientPresentation from "@/components/Organism/CRM/ClientPresentation.vue";
|
import ClientPresentation from "@/components/Organism/CRM/ClientPresentation.vue";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await clientStore.fetchClients();
|
await clientStore.fetchClients();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const goDetails = (id) => {
|
||||||
|
router.push({
|
||||||
|
name: "Client details",
|
||||||
|
params: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,4 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ContactPresentation from "@/components/Organism/CRM/ContactPresentation.vue";
|
import ContactPresentation from "@/components/Organism/CRM/ContactPresentation.vue";
|
||||||
|
import useContactStore from "@/stores/contactStore";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
const contactStore = useContactStore();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
contactStore.fetchContacts();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user