rubis/apps/api/app/mails/relance_email.tsx
ordinarthur eb248c98b8
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
style(mail): fond crème edge-to-edge (au lieu d'un container blanc)
Avant : body crème + container blanc avec border-radius + bordure → effet
"card flottante" qui laissait des marges crème en haut/bas et un fond
blanc pour le contenu. Sur iPhone Mail, cette structure créait un gap
mal rendu entre la frame Mail.app et le header rubis-deep.

Maintenant :
- bodyStyle.padding 24px 0 → 0 (rubis-deep colle au haut de la zone mail)
- containerStyle background blanc → crème (toute la zone email est crème,
  cohérente avec la palette)
- containerStyle borderRadius + border supprimés (edge-to-edge)
- invoiceCardStyle (checkin) + summaryCardStyle (relance) passent en
  blanc pour se détacher du nouveau fond crème
- Dark mode CSS : .rubis-container override aussi mis à crème

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:55:47 +02:00

138 lines
4.2 KiB
TypeScript

/**
* Template email de relance — envoyé AU CLIENT FINAL pour réclamer le
* paiement d'une facture impayée. Le subject + le body sont définis par
* le user dans son plan de relance (avec variables {{numero}} etc.) ; on
* injecte ce body brut dans une mise en page Rubis :
*
* - Header rubis-deep avec le NOM DE L'ORG (ce que connaît le client)
* et le numéro de facture en sous-titre
* - Body rendu (le texte que le user a rédigé, déjà interpolé)
* - Card "récap facture" en pied de body : numéro, montant, échéance
*
* Pas de boutons CTA dans la relance — le client est censé payer hors
* mail (virement, espèces, ...). Le mail rappelle juste le contexte.
*/
import * as React from 'react'
import { Section, Text } from '@react-email/components'
import { BRAND, sp } from './_brand.js'
import { EmailLayout } from './_layout.js'
export type RelanceEmailProps = {
/** Nom commercial visible côté client (l'org du user). */
brandName: string
invoice: {
numero: string
amountFormatted: string
dueDateFormatted: string
daysLate: number
}
/** Texte de la relance (déjà interpolé) que le user a posé dans son plan. */
bodyText: string
/** URL landing publique (footer cliquable "Rubis sur l'ongle"). */
landingUrl?: string
}
export function RelanceEmail({
brandName,
invoice,
bodyText,
landingUrl,
}: RelanceEmailProps) {
const isLate = invoice.daysLate > 0
return (
<EmailLayout
preview={`${invoice.numero} (${invoice.amountFormatted}) — ${
isLate ? `${invoice.daysLate}j de retard` : 'à régler'
}`}
brandName={brandName}
brandSubtitle={`Facture ${invoice.numero}`}
landingUrl={landingUrl}
>
{/* Body texte en pre-line pour conserver les sauts de ligne tels que
le user les a écrits. Le client lit le mot du patron, pas un
template impersonnel. */}
<Text style={bodyTextStyle}>{bodyText}</Text>
{/* Card récap en pied : tableau visuel court qui rappelle les chiffres. */}
<Section style={summaryCardStyle}>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Facture</span>
<span style={summaryValueStyle}>{invoice.numero}</span>
</Text>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Montant TTC</span>
<span
style={{
...summaryValueStyle,
fontSize: '18px',
fontWeight: 800,
fontVariantNumeric: 'tabular-nums',
}}
>
{invoice.amountFormatted}
</span>
</Text>
<Text style={summaryRowStyle}>
<span style={summaryLabelStyle}>Échéance</span>
<span
style={{
...summaryValueStyle,
color: isLate ? BRAND.rubisDeep : BRAND.ink,
}}
>
{invoice.dueDateFormatted}
{isLate ? (
<span style={{ color: BRAND.rubisDeep, fontSize: '12px', marginLeft: sp.sm }}>
({invoice.daysLate}j de retard)
</span>
) : null}
</span>
</Text>
</Section>
</EmailLayout>
)
}
// ---------------------------------------------------------------------------
// Styles inline
// ---------------------------------------------------------------------------
const bodyTextStyle: React.CSSProperties = {
color: BRAND.ink,
fontSize: '15px',
lineHeight: '1.6',
margin: `0 0 ${sp.xl} 0`,
whiteSpace: 'pre-line', // préserve les \n du body sans nécessiter <br/>
}
const summaryCardStyle: React.CSSProperties = {
// Blanc sur fond crème pour détacher la card visuellement (le container
// du layout est désormais crème, pas blanc).
backgroundColor: BRAND.white,
border: `1px solid ${BRAND.line}`,
borderRadius: BRAND.radiusCard,
padding: `${sp.md} ${sp.lg}`,
margin: `${sp.lg} 0 0 0`,
}
const summaryRowStyle: React.CSSProperties = {
display: 'block',
margin: `${sp.sm} 0`,
fontSize: '13px',
lineHeight: '1.4',
}
const summaryLabelStyle: React.CSSProperties = {
display: 'inline-block',
width: '110px',
color: BRAND.ink3,
fontWeight: 500,
}
const summaryValueStyle: React.CSSProperties = {
color: BRAND.ink,
fontWeight: 600,
}