Save France
Composants

ContractSummaryCard

Carte d'affichage du résumé d'un contrat avec informations client et détails contractuels

Description

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.

Localisation

components/shared/ContractSummaryCard.vue

Props

NomTypeRequisDéfautDescription
requestRequestOui-Objet contenant les informations de la demande

Types TypeScript

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

Fonctionnalités

1. Affichage des informations client

Le composant affiche les informations suivantes du client :

  • Nom complet : Prénom et nom de famille
  • Email : Adresse email de contact
  • Adresse complète : Rue, code postal et ville

2. Affichage des détails du contrat

Le composant affiche :

  • Type de contrat : Nom de l'offre de maintenance
  • Prix : Adapté automatiquement selon le type de client
    • Pour les professionnels (isPro: true) : Prix HT
    • Pour les particuliers : Prix TTC
  • Nombre d'équipements : Comptage automatique des équipements inclus

3. Formatage automatique

  • Prix : Conversion automatique des centimes en euros avec 2 décimales
    • Entrée : 12000 (centimes)
    • Sortie : 120.00€/mois
  • Pluriel : Adaptation automatique pour les équipements
    • 0 ou 1 : X équipement(s)
    • 2+ : X équipements

Utilisation

Exemple basique

<template>
  <ContractSummaryCard :request="request" />
</template>

<script setup lang="ts">
const { data: request } = await useFetch(`/api/requests/${reqID}`, {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
</script>

Avec classe personnalisée

<template>
  <ContractSummaryCard :request="request" class="mb-8" />
</template>

Dans une page de contrat

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

Style

Le composant utilise une présentation en grille responsive :

  • Mobile : Une colonne (informations empilées)
  • Desktop : Deux colonnes (client à gauche, contrat à droite)

Structure visuelle

┌─────────────────────────────────────────┐
│  Détails du contrat                     │
├────────────────┬────────────────────────┤
│ Client         │ Contrat                │
│                │                        │
│ Nom: ...       │ Type: ...              │
│ Email: ...     │ Prix: ...              │
│ Adresse: ...   │ Équipements: ...       │
└────────────────┴────────────────────────┘

Classes CSS utilisées

  • bg-gray-50 dark:bg-gray-800 : Fond adaptatif au thème
  • rounded-lg : Coins arrondis
  • p-6 : Padding interne
  • grid grid-cols-1 md:grid-cols-2 : Grille responsive
  • gap-6 : Espacement entre les colonnes

Gestion des données manquantes

Le composant gère gracieusement les données manquantes :

  • Si request.offer?.maintenanceOffer?.name est absent : affiche "N/A"
  • Si request.equipments est absent : affiche 0 équipement(s)
  • Si les prix sont manquants : affiche 0€/mois

Exemple de données

const 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)

Bonnes pratiques

1. Vérification des données

Toujours vérifier que request existe avant d'utiliser le composant :

<ContractSummaryCard v-if="request" :request="request" />

2. Gestion du chargement

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>

3. Mise à jour des données

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>

Accessibilité

  • Les labels utilisent des <span> avec des classes appropriées pour la différenciation visuelle
  • Structure sémantique avec des titres <h4> et <h5>
  • Contraste suffisant entre le texte et l'arrière-plan

Pages utilisant ce composant

  • [reqID]/contract.vue : Page de gestion de contrat
  • [reqID]/refus-souscription.vue : Page de questionnaire de non-souscription

Tests

Tests unitaires recommandés

import { 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');
  });
});

Dépannage

Le prix ne s'affiche pas correctement

Vérifiez que les prix sont en centimes :

// ✅ Correct
offer: { price: { ttc: 15000 } } // 150.00€

// ❌ Incorrect
offer: { price: { ttc: 150 } } // 1.50€

Les données ne s'affichent pas

Vérifiez la structure de l'objet request dans la console :

console.log('Request data:', request);

Évolutions futures

  • Ajouter un mode compact
  • Permettre de masquer certaines sections
  • Ajouter des icônes pour chaque information
  • Support de l'impression