add client detail

This commit is contained in:
Nyavokevin 2025-10-10 19:00:12 +03:00
parent 175446adbe
commit c5a4fcc546
24 changed files with 2181 additions and 511 deletions

View File

@ -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',

View File

@ -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

View 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`

View 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`

View 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 ! 🎉

View File

@ -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([

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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");

View File

@ -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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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> &nbsp;
<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>

View File

@ -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>

View File

@ -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>

View 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;

View 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>

View File

@ -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({

View File

@ -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;

View 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;

View 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;

View 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>

View File

@ -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>

View File

@ -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>