add client detail
This commit is contained in:
parent
175446adbe
commit
c5a4fcc546
@ -22,7 +22,7 @@ class UpdateClientRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre',
|
||||
|
||||
'name' => 'required|string|max:255',
|
||||
'vat_number' => 'nullable|string|max:32',
|
||||
'siret' => 'nullable|string|max:20',
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# API base URL for axios (used by src/services/http.ts)
|
||||
# For Laravel Sanctum on local dev (default Laravel port):
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000
|
||||
NODE_ENV=production
|
||||
|
||||
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>
|
||||
<!-- Conteneur de notifications global -->
|
||||
<notification-container />
|
||||
|
||||
<!-- Guest Layout: for login, signup, auth pages -->
|
||||
<div v-if="isGuestRoute" class="guest-layout">
|
||||
<router-view />
|
||||
@ -35,6 +38,7 @@ import Sidenav from "./examples/Sidenav";
|
||||
import Configurator from "@/examples/Configurator.vue";
|
||||
import Navbar from "@/examples/Navbars/Navbar.vue";
|
||||
import AppFooter from "@/examples/Footer.vue";
|
||||
import NotificationContainer from "@/components/NotificationContainer.vue";
|
||||
import { mapMutations, mapState } from "vuex";
|
||||
export default {
|
||||
name: "App",
|
||||
@ -43,6 +47,7 @@ export default {
|
||||
Configurator,
|
||||
Navbar,
|
||||
AppFooter,
|
||||
NotificationContainer,
|
||||
},
|
||||
computed: {
|
||||
...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 />
|
||||
</template>
|
||||
<template #client-table>
|
||||
<client-table :data="clientData" :loading="loadingData" />
|
||||
<client-table
|
||||
:data="clientData"
|
||||
:loading="loadingData"
|
||||
@view="goToDetails"
|
||||
@delete="deleteClient"
|
||||
/>
|
||||
</template>
|
||||
</client-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 FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||
import { defineProps } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits(["pushDetails"]);
|
||||
|
||||
defineProps({
|
||||
clientData: {
|
||||
type: Array,
|
||||
@ -41,4 +48,12 @@ const goToClient = () => {
|
||||
name: "Creation client",
|
||||
});
|
||||
};
|
||||
|
||||
const goToDetails = (client) => {
|
||||
emit("pushDetails", client);
|
||||
};
|
||||
|
||||
const deleteClient = (client) => {
|
||||
emit("deleteClient", client);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<table-action />
|
||||
</template>
|
||||
<template #contact-table>
|
||||
<contact-table />
|
||||
<contact-table :contacts-data="contacts" />
|
||||
</template>
|
||||
</contact-template>
|
||||
</template>
|
||||
@ -20,4 +20,12 @@ import ContactTable from "@/components/molecules/Tables/ContactTable.vue";
|
||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -91,7 +91,6 @@
|
||||
<tbody>
|
||||
<tr v-for="client in data" :key="client.id">
|
||||
<!-- Commercial Column -->
|
||||
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox />
|
||||
@ -163,6 +162,33 @@
|
||||
<span>{{ client.is_active ? "Active" : "Inactive" }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -173,21 +199,25 @@
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-users fa-3x text-muted"></i>
|
||||
</div>
|
||||
<h5 class="empty-title">No clients found</h5>
|
||||
"
|
||||
<h5 class="empty-title">Aucun client trouvé</h5>
|
||||
<p class="empty-text text-muted">
|
||||
There are no clients to display at the moment.
|
||||
Aucun client à afficher pour le moment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
import SoftButton from "@/components/SoftButton.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
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
@ -252,6 +282,7 @@ const initializeDataTable = () => {
|
||||
// Destroy existing instance if it exists
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
dataTableInstance.value = null;
|
||||
}
|
||||
|
||||
const dataTableEl = document.getElementById("contact-list");
|
||||
@ -262,6 +293,22 @@ const initializeDataTable = () => {
|
||||
perPage: 10,
|
||||
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 }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
const dataTableEl = document.getElementById("contact-list");
|
||||
if (dataTableEl) {
|
||||
dataTableEl.removeEventListener("click", handleTableClick);
|
||||
}
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize data
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
@ -425,4 +482,13 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
@ -3,516 +3,28 @@
|
||||
<table id="order-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Date de creation</th>
|
||||
<th>Nom</th>
|
||||
<th>Customer</th>
|
||||
<th>Product</th>
|
||||
<th>Revenue</th>
|
||||
<th>Contact</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Date Creation</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
import img2 from "@/assets/img/team-1.jpg";
|
||||
import img3 from "@/assets/img/team-3.jpg";
|
||||
import img4 from "@/assets/img/team-4.jpg";
|
||||
import img5 from "@/assets/img/team-5.jpg";
|
||||
import img6 from "@/assets/img/ivana-squares.jpg";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
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",
|
||||
component: () => import("@/views/pages/CRM/AddClient.vue"),
|
||||
},
|
||||
{
|
||||
path: "/crm/client/:id",
|
||||
name: "Client details",
|
||||
component: () => import("@/views/pages/CRM/ClientDetails.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
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-data="clientStore.clients"
|
||||
:loading-data="clientStore.loading"
|
||||
@push-details="goDetails"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ClientPresentation from "@/components/Organism/CRM/ClientPresentation.vue";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const clientStore = useClientStore();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
await clientStore.fetchClients();
|
||||
});
|
||||
|
||||
const goDetails = (id) => {
|
||||
router.push({
|
||||
name: "Client details",
|
||||
params: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -3,4 +3,12 @@
|
||||
</template>
|
||||
<script setup>
|
||||
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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user