Composant d'affichage du résumé d'un contrat avec les informations du client et les détails du contrat. Ce composant est utilisé sur plusieurs pages pour maintenir une présentation cohérente des informations contractuelles.
components/shared/ContractSummaryCard.vue
| Nom | Type | Requis | Défaut | Description |
|---|---|---|---|---|
| request | Request | Oui | - | Objet contenant les informations de la demande |
interface Contact {
name?: string;
lastname?: string;
email?: string;
street?: string;
postalCode?: string;
city?: string;
isPro?: boolean;
}
interface Price {
ht?: number; // Prix en centimes HT
ttc?: number; // Prix en centimes TTC
}
interface Offer {
price?: Price;
maintenanceOffer?: {
name?: string;
};
}
interface Request {
contact?: Contact;
offer?: Offer;
equipments?: Array<any>;
}
Le composant affiche les informations suivantes du client :
Le composant affiche :
isPro: true) : Prix HT12000 (centimes)120.00€/moisX équipement(s)X équipements<template>
<ContractSummaryCard :request="request" />
</template>
<script setup lang="ts">
const { data: request } = await useFetch(`/api/requests/${reqID}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
</script>
<template>
<ContractSummaryCard :request="request" class="mb-8" />
</template>
<template>
<UCard class="max-w-6xl mx-auto">
<h2>Détails de votre demande</h2>
<!-- Résumé du contrat -->
<ContractSummaryCard :request="request" class="mb-8" />
<!-- Actions -->
<div class="flex gap-4">
<UButton @click="handleAction">Action</UButton>
</div>
</UCard>
</template>
Le composant utilise une présentation en grille responsive :
┌─────────────────────────────────────────┐
│ Détails du contrat │
├────────────────┬────────────────────────┤
│ Client │ Contrat │
│ │ │
│ Nom: ... │ Type: ... │
│ Email: ... │ Prix: ... │
│ Adresse: ... │ Équipements: ... │
└────────────────┴────────────────────────┘
bg-gray-50 dark:bg-gray-800 : Fond adaptatif au thèmerounded-lg : Coins arrondisp-6 : Padding internegrid grid-cols-1 md:grid-cols-2 : Grille responsivegap-6 : Espacement entre les colonnesLe composant gère gracieusement les données manquantes :
request.offer?.maintenanceOffer?.name est absent : affiche "N/A"request.equipments est absent : affiche 0 équipement(s)0€/moisconst sampleRequest = {
contact: {
name: "Jean",
lastname: "Dupont",
email: "jean.dupont@example.com",
street: "123 Rue de la Paix",
postalCode: "75001",
city: "Paris",
isPro: false
},
offer: {
price: {
ht: 15000, // 150€ HT
ttc: 18000 // 180€ TTC
},
maintenanceOffer: {
name: "Contrat Premium"
}
},
equipments: [
{ id: 1, name: "Chaudière" },
{ id: 2, name: "Radiateur" }
]
};
Affichage résultant :
Détails du contrat
Client Contrat
Nom: Jean Dupont Type: Contrat Premium
Email: jean.dupont@... Prix: 180.00€/mois
Adresse: 123 Rue de... Équipements: 2 équipement(s)
Toujours vérifier que request existe avant d'utiliser le composant :
<ContractSummaryCard v-if="request" :request="request" />
Afficher un état de chargement pendant la récupération des données :
<template>
<div v-if="pending">Chargement...</div>
<ContractSummaryCard v-else-if="request" :request="request" />
<div v-else>Erreur de chargement</div>
</template>
<script setup>
const { data: request, pending } = await useFetch('/api/requests/123');
</script>
Si les données peuvent changer, utiliser refresh :
<script setup>
const { data: request, refresh } = await useFetch('/api/requests/123');
const updateContract = async () => {
await $fetch('/api/requests/123', { method: 'PATCH', body: {...} });
await refresh(); // Rafraîchit les données affichées
};
</script>
<span> avec des classes appropriées pour la différenciation visuelle<h4> et <h5>[reqID]/contract.vue : Page de gestion de contrat[reqID]/refus-souscription.vue : Page de questionnaire de non-souscriptionimport { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import ContractSummaryCard from '~/components/shared/ContractSummaryCard.vue';
describe('ContractSummaryCard', () => {
it('affiche les informations du client', () => {
const request = {
contact: {
name: 'Jean',
lastname: 'Dupont',
email: 'jean@example.com'
}
};
const wrapper = mount(ContractSummaryCard, {
props: { request }
});
expect(wrapper.text()).toContain('Jean Dupont');
expect(wrapper.text()).toContain('jean@example.com');
});
it('affiche le prix HT pour les professionnels', () => {
const request = {
contact: { isPro: true },
offer: { price: { ht: 15000 } }
};
const wrapper = mount(ContractSummaryCard, {
props: { request }
});
expect(wrapper.text()).toContain('150.00€/mois HT');
});
it('affiche le prix TTC pour les particuliers', () => {
const request = {
contact: { isPro: false },
offer: { price: { ttc: 18000 } }
};
const wrapper = mount(ContractSummaryCard, {
props: { request }
});
expect(wrapper.text()).toContain('180.00€/mois');
expect(wrapper.text()).not.toContain('HT');
});
});
Vérifiez que les prix sont en centimes :
// ✅ Correct
offer: { price: { ttc: 15000 } } // 150.00€
// ❌ Incorrect
offer: { price: { ttc: 150 } } // 1.50€
Vérifiez la structure de l'objet request dans la console :
console.log('Request data:', request);