All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
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>
138 lines
4.2 KiB
TypeScript
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,
|
|
}
|