@consilioweb/payload-support 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1525,17 +1525,31 @@ function createSlaCheckEndpoint(slugs) {
1525
1525
  }
1526
1526
 
1527
1527
  // src/utils/emailTemplate.ts
1528
+ var COLORS = {
1529
+ ink: "#1d2b4d",
1530
+ inkSoft: "#3a486a",
1531
+ teal: "#17807c",
1532
+ amber: "#c2410c",
1533
+ paper: "#faf8f5",
1534
+ card: "#ffffff",
1535
+ sand: "#f4f1ec",
1536
+ line: "#e7e2d9",
1537
+ text: "#2a3142",
1538
+ muted: "#6b7280",
1539
+ faint: "#9aa1ad"
1540
+ };
1528
1541
  var DEFAULT_CONFIG = {
1529
1542
  brandName: "Support",
1530
- brandColor: "#00E5FF",
1531
- secondaryColor: "#FFD600",
1532
- accentColor: "#FF8A00",
1543
+ brandColor: COLORS.teal,
1544
+ secondaryColor: COLORS.ink,
1545
+ accentColor: COLORS.amber,
1533
1546
  logoUrl: "",
1534
1547
  supportEmail: process.env.SUPPORT_EMAIL || "",
1535
1548
  websiteUrl: process.env.NEXT_PUBLIC_SERVER_URL || "",
1536
1549
  phone: "",
1537
1550
  location: "",
1538
- brandInitials: ""
1551
+ brandInitials: "",
1552
+ preferencesUrl: ""
1539
1553
  };
1540
1554
  function resolveConfig(config) {
1541
1555
  return { ...DEFAULT_CONFIG, ...config };
@@ -1574,30 +1588,30 @@ function emailRichContent(html, config) {
1574
1588
  }
1575
1589
  let result = html.replace(/src="\/([^"]+)"/g, `src="${baseUrl}/$1"`);
1576
1590
  result = convertFencedCodeBlocks(result);
1577
- result = styleTag(result, "blockquote", `border-left: 4px solid ${c.brandColor}; margin: 16px 0; padding: 12px 20px; background: #f0f9fa; border-radius: 0 8px 8px 0;`);
1591
+ result = styleTag(result, "blockquote", `border-left: 3px solid ${COLORS.teal}; margin: 16px 0; padding: 12px 20px; background: ${COLORS.sand}; border-radius: 0 8px 8px 0; color: ${COLORS.inkSoft};`);
1578
1592
  result = styleTag(result, "img", "max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; display: block;");
1579
- result = styleTag(result, "a", `color: ${c.brandColor}; text-decoration: underline; font-weight: 600;`);
1593
+ result = styleTag(result, "a", `color: ${COLORS.teal}; text-decoration: underline; font-weight: 600;`);
1580
1594
  result = styleTag(result, "ul", "margin: 8px 0; padding-left: 24px;");
1581
1595
  result = styleTag(result, "ol", "margin: 8px 0; padding-left: 24px;");
1582
- result = styleTag(result, "li", "margin: 4px 0; line-height: 1.6;");
1583
- result = styleTag(result, "p", "margin: 0 0 12px 0; line-height: 1.75; font-size: 15px; color: #1f2937;");
1584
- result = result.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/\bon\w+\s*=\s*"[^"]*"/gi, "").replace(/\bon\w+\s*=\s*'[^']*'/gi, "").replace(/javascript:/gi, "");
1585
- return `<div style="font-size: 15px; line-height: 1.75; color: #1f2937;">${result}</div>`;
1596
+ result = styleTag(result, "li", `margin: 4px 0; line-height: 1.6; color: ${COLORS.text};`);
1597
+ result = styleTag(result, "p", `margin: 0 0 12px 0; line-height: 1.75; font-size: 15px; color: ${COLORS.text};`);
1598
+ result = result.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "").replace(/<iframe\b[^>]*\/?>/gi, "").replace(/<embed\b[^>]*\/?>/gi, "").replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "").replace(/\bon\w+\s*=\s*"[^"]*"/gi, "").replace(/\bon\w+\s*=\s*'[^']*'/gi, "").replace(/\bon\w+\s*=\s*[^\s>]+/gi, "").replace(/javascript:/gi, "");
1599
+ return `<div style="font-size: 15px; line-height: 1.75; color: ${COLORS.text};">${result}</div>`;
1586
1600
  }
1587
- function getButtonColors(config) {
1601
+ function getButtonColors(_config) {
1588
1602
  return {
1589
- primary: { bg: config.brandColor, text: "#000000", border: "#000000" },
1590
- secondary: { bg: config.secondaryColor, text: "#000000", border: "#000000" },
1591
- dark: { bg: "#000000", text: "#FFFFFF", border: "#000000" }
1603
+ primary: { bg: COLORS.teal, text: "#ffffff", border: COLORS.teal },
1604
+ secondary: { bg: COLORS.sand, text: COLORS.ink, border: COLORS.line },
1605
+ dark: { bg: COLORS.ink, text: "#ffffff", border: COLORS.ink }
1592
1606
  };
1593
1607
  }
1594
1608
  function emailButton(text, url, color = "primary", config) {
1595
- const c = resolveConfig(config);
1596
- const colors = getButtonColors(c);
1609
+ resolveConfig(config);
1610
+ const colors = getButtonColors();
1597
1611
  const bc = colors[color];
1598
1612
  return `
1599
1613
  <div style="text-align: center; margin: 32px 0;">
1600
- <a href="${url}" style="display: inline-block; padding: 16px 40px; background: ${bc.bg}; color: ${bc.text}; font-weight: 800; font-size: 15px; text-decoration: none; border-radius: 10px; border: 2px solid ${bc.border}; letter-spacing: 0.02em;">
1614
+ <a href="${url}" style="display: inline-block; padding: 14px 36px; background: ${bc.bg}; color: ${bc.text}; font-weight: 700; font-size: 15px; text-decoration: none; border-radius: 10px; border: 1px solid ${bc.border}; letter-spacing: 0.01em;">
1601
1615
  ${text}
1602
1616
  </a>
1603
1617
  </div>
@@ -1612,8 +1626,9 @@ function renderCodeBlockHtml(lang, code) {
1612
1626
  </div>`;
1613
1627
  }
1614
1628
  function emailQuote(content, borderColor, config) {
1615
- const c = resolveConfig(config);
1616
- const color = borderColor || c.brandColor;
1629
+ resolveConfig(config);
1630
+ const color = borderColor || COLORS.teal;
1631
+ const panel = `margin: 24px 0; padding: 18px 22px; background: ${COLORS.sand}; border-left: 3px solid ${color}; border-radius: 0 8px 8px 0;`;
1617
1632
  if (content && content.includes("```")) {
1618
1633
  const parts = [];
1619
1634
  const regex = /```(\w*)\n?([\s\S]*?)```/g;
@@ -1622,62 +1637,77 @@ function emailQuote(content, borderColor, config) {
1622
1637
  while ((match = regex.exec(content)) !== null) {
1623
1638
  const textBefore = content.slice(lastIndex, match.index).trim();
1624
1639
  if (textBefore) {
1625
- parts.push(`<p style="margin: 0 0 12px 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textBefore)}</p>`);
1640
+ parts.push(`<p style="margin: 0 0 12px 0; font-size: 15px; line-height: 1.75; color: ${COLORS.inkSoft}; white-space: pre-wrap;">${escapeHtml(textBefore)}</p>`);
1626
1641
  }
1627
1642
  parts.push(renderCodeBlockHtml(match[1] || "", match[2] || ""));
1628
1643
  lastIndex = match.index + match[0].length;
1629
1644
  }
1630
1645
  const textAfter = content.slice(lastIndex).trim();
1631
1646
  if (textAfter) {
1632
- parts.push(`<p style="margin: 12px 0 0 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textAfter)}</p>`);
1647
+ parts.push(`<p style="margin: 12px 0 0 0; font-size: 15px; line-height: 1.75; color: ${COLORS.inkSoft}; white-space: pre-wrap;">${escapeHtml(textAfter)}</p>`);
1633
1648
  }
1634
1649
  return `
1635
- <div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
1650
+ <div style="${panel}">
1636
1651
  ${parts.join("")}
1637
1652
  </div>
1638
1653
  `;
1639
1654
  }
1640
1655
  return `
1641
- <div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
1642
- <p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
1656
+ <div style="${panel}">
1657
+ <p style="margin: 0; font-size: 15px; line-height: 1.75; color: ${COLORS.inkSoft}; white-space: pre-wrap;">${escapeHtml(content)}</p>
1643
1658
  </div>
1644
1659
  `;
1645
1660
  }
1646
1661
  function emailParagraph(text) {
1647
- return `<p style="margin: 0 0 18px 0; font-size: 15px; line-height: 1.75; color: #1f2937;">${text}</p>`;
1662
+ return `<p style="margin: 0 0 18px 0; font-size: 15px; line-height: 1.75; color: ${COLORS.text};">${text}</p>`;
1663
+ }
1664
+ function getKindStyle(kind) {
1665
+ switch (kind) {
1666
+ case "reply":
1667
+ return { eyebrow: "R\xE9ponse de votre \xE9quipe", accent: COLORS.teal };
1668
+ case "alert":
1669
+ return { eyebrow: "Action requise", accent: COLORS.amber };
1670
+ case "system":
1671
+ default:
1672
+ return { eyebrow: "Notification automatique", accent: COLORS.inkSoft };
1673
+ }
1648
1674
  }
1649
1675
  function emailFooter(config) {
1650
1676
  const logoHtml = config.logoUrl ? `<a href="${config.websiteUrl}">
1651
1677
  <img src="${config.logoUrl}" alt="${escapeHtml(config.brandName)}" width="100" height="47" style="display: block; border: 0;" />
1652
- </a>` : `<a href="${config.websiteUrl}" style="font-size: 18px; font-weight: 900; color: #000000; text-decoration: none;">${escapeHtml(config.brandName)}</a>`;
1678
+ </a>` : `<a href="${config.websiteUrl}" style="font-size: 16px; font-weight: 800; color: ${COLORS.ink}; text-decoration: none; letter-spacing: -0.01em;">${escapeHtml(config.brandName)}</a>`;
1653
1679
  const contactParts = [];
1654
1680
  if (config.supportEmail) {
1655
- contactParts.push(`<a href="mailto:${config.supportEmail}" style="color: #555555; text-decoration: none;">${config.supportEmail}</a>`);
1681
+ contactParts.push(`<a href="mailto:${config.supportEmail}" style="color: ${COLORS.muted}; text-decoration: none;">${config.supportEmail}</a>`);
1656
1682
  }
1657
1683
  if (config.phone) {
1658
- contactParts.push(`<a href="tel:${config.phone.replace(/\s/g, "")}" style="color: #555555; text-decoration: none;">${config.phone}</a>`);
1684
+ contactParts.push(`<a href="tel:${config.phone.replace(/\s/g, "")}" style="color: ${COLORS.muted}; text-decoration: none;">${config.phone}</a>`);
1659
1685
  }
1660
1686
  const locationParts = [];
1661
1687
  if (config.websiteUrl) {
1662
1688
  const displayUrl = config.websiteUrl.replace(/^https?:\/\//, "");
1663
- locationParts.push(`<a href="${config.websiteUrl}" style="color: ${config.brandColor}; text-decoration: none; font-weight: 600;">${displayUrl}</a>`);
1689
+ locationParts.push(`<a href="${config.websiteUrl}" style="color: ${COLORS.teal}; text-decoration: none; font-weight: 600;">${displayUrl}</a>`);
1664
1690
  }
1665
1691
  if (config.location) {
1666
1692
  locationParts.push(config.location);
1667
1693
  }
1668
- const unsubscribeHtml = config.supportEmail ? `<p style="margin: 8px 0 0 0; font-size: 11px; color: #aaaaaa; line-height: 1.4;">
1669
- <a href="mailto:${config.supportEmail}?subject=Unsubscribe" style="color: #aaaaaa; text-decoration: underline;">Se d&eacute;sinscrire</a>
1670
- </p>` : "";
1694
+ const manageUrl = config.preferencesUrl || (config.websiteUrl ? `${config.websiteUrl}/support/profile` : "") || (config.supportEmail ? `mailto:${config.supportEmail}?subject=Notifications` : "");
1695
+ const preferencesLinks = [];
1696
+ if (manageUrl) {
1697
+ preferencesLinks.push(`<a href="${manageUrl}" style="color: ${COLORS.muted}; text-decoration: underline;">G\xE9rer mes notifications</a>`);
1698
+ }
1699
+ if (config.supportEmail) {
1700
+ preferencesLinks.push(`<a href="mailto:${config.supportEmail}?subject=Unsubscribe" style="color: ${COLORS.faint}; text-decoration: underline;">Se d\xE9sinscrire</a>`);
1701
+ }
1702
+ const preferencesHtml = preferencesLinks.length > 0 ? `<p style="margin: 10px 0 0 0; font-size: 11px; color: ${COLORS.faint}; line-height: 1.5;">${preferencesLinks.join(" &nbsp;&middot;&nbsp; ")}</p>` : "";
1671
1703
  return `
1672
1704
  <!-- Spacer -->
1673
- <div style="height: 24px;"></div>
1705
+ <div style="height: 28px;"></div>
1674
1706
 
1675
- <!-- Tricolor separator -->
1676
- <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 24px;">
1707
+ <!-- Hairline separator -->
1708
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 22px;">
1677
1709
  <tr>
1678
- <td width="50%" height="3" bgcolor="${config.brandColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1679
- <td width="25%" height="3" bgcolor="${config.secondaryColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1680
- <td width="25%" height="3" bgcolor="${config.accentColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1710
+ <td height="1" bgcolor="${COLORS.line}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1681
1711
  </tr>
1682
1712
  </table>
1683
1713
 
@@ -1688,10 +1718,10 @@ function emailFooter(config) {
1688
1718
  ${logoHtml}
1689
1719
  </td>
1690
1720
  <td valign="top">
1691
- <p style="margin: 0; font-size: 14px; font-weight: 800; color: #000000; letter-spacing: 0.01em;">${escapeHtml(config.brandName)}</p>
1692
- ${contactParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 13px; color: #555555; line-height: 1.5;">${contactParts.join(" &nbsp;&middot;&nbsp; ")}</p>` : ""}
1693
- ${locationParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 12px; color: #888888; line-height: 1.4;">${locationParts.join(" &nbsp;&middot;&nbsp; ")}</p>` : ""}
1694
- ${unsubscribeHtml}
1721
+ <p style="margin: 0; font-size: 13px; font-weight: 700; color: ${COLORS.ink}; letter-spacing: 0.01em;">${escapeHtml(config.brandName)}</p>
1722
+ ${contactParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 13px; color: ${COLORS.muted}; line-height: 1.5;">${contactParts.join(" &nbsp;&middot;&nbsp; ")}</p>` : ""}
1723
+ ${locationParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 12px; color: ${COLORS.faint}; line-height: 1.4;">${locationParts.join(" &nbsp;&middot;&nbsp; ")}</p>` : ""}
1724
+ ${preferencesHtml}
1695
1725
  </td>
1696
1726
  </tr>
1697
1727
  </table>
@@ -1699,11 +1729,13 @@ function emailFooter(config) {
1699
1729
  }
1700
1730
  function emailWrapper(title, body, options = {}, config) {
1701
1731
  const c = resolveConfig(config);
1702
- const { headerColor = "primary", preheader } = options;
1703
- const headerBg = headerColor === "secondary" ? c.secondaryColor : c.brandColor;
1732
+ const { preheader, kind } = options;
1733
+ const kindStyle = kind ? getKindStyle(kind) : null;
1734
+ const accent = kindStyle?.accent || COLORS.teal;
1735
+ const eyebrowHtml = kindStyle ? `<p style="margin: 0 0 6px 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: ${kindStyle.accent};">${escapeHtml(kindStyle.eyebrow)}</p>` : "";
1704
1736
  const badgeHtml = c.brandInitials ? `<td width="48" align="right" valign="middle">
1705
- <div style="width: 36px; height: 36px; border-radius: 8px; background: #000; display: inline-block; text-align: center; line-height: 36px;">
1706
- <span style="color: #fff; font-weight: 900; font-size: 14px; letter-spacing: 0.05em;">${escapeHtml(c.brandInitials)}</span>
1737
+ <div style="width: 38px; height: 38px; border-radius: 10px; background: ${COLORS.ink}; display: inline-block; text-align: center; line-height: 38px;">
1738
+ <span style="color: #fff; font-weight: 800; font-size: 14px; letter-spacing: 0.04em;">${escapeHtml(c.brandInitials)}</span>
1707
1739
  </div>
1708
1740
  </td>` : "";
1709
1741
  return `<!DOCTYPE html>
@@ -1718,23 +1750,29 @@ function emailWrapper(title, body, options = {}, config) {
1718
1750
  </style>
1719
1751
  <![endif]-->
1720
1752
  </head>
1721
- <body style="margin: 0; padding: 0; background: #f0f0f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
1722
- ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">${escapeHtml(preheader)}</div>` : ""}
1753
+ <body style="margin: 0; padding: 0; background: ${COLORS.paper}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
1754
+ ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all; font-size: 1px; line-height: 1px; color: ${COLORS.paper};">${escapeHtml(preheader)}&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;</div>` : ""}
1723
1755
 
1724
1756
  <!-- Outer wrapper -->
1725
- <table cellpadding="0" cellspacing="0" border="0" width="100%" style="background: #f0f0f0; padding: 40px 16px;">
1757
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="background: ${COLORS.paper}; padding: 40px 16px;">
1726
1758
  <tr>
1727
1759
  <td align="center">
1728
- <!-- Main container \u2014 wider (660px) -->
1729
- <table cellpadding="0" cellspacing="0" border="0" width="660" style="max-width: 660px; width: 100%;">
1760
+ <!-- Main container \u2014 640px -->
1761
+ <table cellpadding="0" cellspacing="0" border="0" width="640" style="max-width: 640px; width: 100%;">
1762
+
1763
+ <!-- Accent rule -->
1764
+ <tr>
1765
+ <td style="height: 4px; background: ${accent}; border-radius: 12px 12px 0 0; font-size: 1px; line-height: 1px;">&nbsp;</td>
1766
+ </tr>
1730
1767
 
1731
1768
  <!-- Header -->
1732
1769
  <tr>
1733
- <td style="background: ${headerBg}; padding: 32px 40px; border-radius: 12px 12px 0 0; border: 2px solid #000000; border-bottom: none;">
1770
+ <td style="background: ${COLORS.card}; padding: 30px 40px 22px 40px; border: 1px solid ${COLORS.line}; border-top: none; border-bottom: none;">
1734
1771
  <table cellpadding="0" cellspacing="0" border="0" width="100%">
1735
1772
  <tr>
1736
- <td>
1737
- <h1 style="margin: 0; color: #000000; font-size: 22px; font-weight: 800; line-height: 1.3; letter-spacing: -0.01em;">
1773
+ <td valign="middle">
1774
+ ${eyebrowHtml}
1775
+ <h1 style="margin: 0; color: ${COLORS.ink}; font-size: 21px; font-weight: 800; line-height: 1.3; letter-spacing: -0.01em;">
1738
1776
  ${escapeHtml(title)}
1739
1777
  </h1>
1740
1778
  </td>
@@ -1746,7 +1784,7 @@ function emailWrapper(title, body, options = {}, config) {
1746
1784
 
1747
1785
  <!-- Body -->
1748
1786
  <tr>
1749
- <td style="background: #ffffff; padding: 40px; border: 2px solid #000000; border-top: none; border-radius: 0 0 12px 12px;">
1787
+ <td style="background: ${COLORS.card}; padding: 8px 40px 40px 40px; border: 1px solid ${COLORS.line}; border-top: none; border-radius: 0 0 12px 12px;">
1750
1788
  ${body}
1751
1789
  ${emailFooter(c)}
1752
1790
  </td>
@@ -5925,8 +5963,12 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
5925
5963
  var STATUS_LABELS = {
5926
5964
  open: "Ouvert",
5927
5965
  waiting_client: "En attente client",
5928
- resolved: "Resolu"
5966
+ waiting_support: "En attente support",
5967
+ resolved: "Resolu",
5968
+ closed: "Ferme",
5969
+ escalated: "Escalade"
5929
5970
  };
5971
+ var SILENT_STATUSES = /* @__PURE__ */ new Set(["waiting_client", "waiting_support", "resolved"]);
5930
5972
  async function resolveClient(doc, payload, clientSlug) {
5931
5973
  const client = doc.client;
5932
5974
  if (!client) return null;
@@ -5978,21 +6020,24 @@ function createTicketStatusEmail(slugs) {
5978
6020
  emailParagraph(`Nous avons bien recu votre demande de support. Voici les details :`),
5979
6021
  `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
5980
6022
  <tr>
5981
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">N&deg; Ticket</td>
5982
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6023
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">N&deg; Ticket</td>
6024
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
5983
6025
  </tr>
5984
6026
  <tr>
5985
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">Sujet</td>
5986
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6027
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Sujet</td>
6028
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
5987
6029
  </tr>
5988
6030
  <tr>
5989
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">Statut</td>
5990
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${STATUS_LABELS[doc.status] || doc.status}</td>
6031
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Statut</td>
6032
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(STATUS_LABELS[doc.status] || String(doc.status))}</td>
5991
6033
  </tr>
5992
6034
  </table>`,
5993
6035
  emailParagraph("Notre equipe vous repondra dans les meilleurs delais."),
5994
6036
  emailButton("Consulter le ticket", ticketUrl)
5995
- ].join(""))
6037
+ ].join(""), {
6038
+ kind: "system",
6039
+ preheader: `Votre demande ${ticketNumber} est bien enregistree. Nous revenons vers vous rapidement.`
6040
+ })
5996
6041
  });
5997
6042
  payload.logger.info(`[tickets] Creation notification sent to ${client.email} for ${ticketNumber}`);
5998
6043
  } catch (error) {
@@ -6001,7 +6046,7 @@ function createTicketStatusEmail(slugs) {
6001
6046
  return doc;
6002
6047
  }
6003
6048
  if (operation === "update" && previousDoc?.status !== doc.status) {
6004
- if (doc.status === "resolved") return doc;
6049
+ if (SILENT_STATUSES.has(doc.status)) return doc;
6005
6050
  try {
6006
6051
  const client = await resolveClient(doc, payload, slugs.supportClients);
6007
6052
  if (!client?.email) return doc;
@@ -6011,12 +6056,6 @@ function createTicketStatusEmail(slugs) {
6011
6056
  const statusLabel = STATUS_LABELS[doc.status] || doc.status;
6012
6057
  const previousStatusLabel = STATUS_LABELS[previousDoc?.status] || previousDoc?.status || "N/A";
6013
6058
  const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`;
6014
- let contextMessage = "";
6015
- if (doc.status === "waiting_client") {
6016
- contextMessage = emailParagraph(
6017
- "Nous avons besoin d'informations supplementaires de votre part pour continuer le traitement de votre demande. Merci de consulter le ticket et de nous repondre."
6018
- );
6019
- }
6020
6059
  await payload.sendEmail({
6021
6060
  to: client.email,
6022
6061
  ...supportEmail ? { replyTo: supportEmail } : {},
@@ -6026,25 +6065,27 @@ function createTicketStatusEmail(slugs) {
6026
6065
  emailParagraph(`Le statut de votre ticket a ete mis a jour :`),
6027
6066
  `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
6028
6067
  <tr>
6029
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">N&deg; Ticket</td>
6030
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6068
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">N&deg; Ticket</td>
6069
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6031
6070
  </tr>
6032
6071
  <tr>
6033
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">Sujet</td>
6034
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6072
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Sujet</td>
6073
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6035
6074
  </tr>
6036
6075
  <tr>
6037
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">Ancien statut</td>
6038
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(previousStatusLabel))}</td>
6076
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Ancien statut</td>
6077
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(previousStatusLabel))}</td>
6039
6078
  </tr>
6040
6079
  <tr>
6041
- <td style="padding: 8px 0; font-size: 12px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em;">Nouveau statut</td>
6042
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(statusLabel))}</strong></td>
6080
+ <td style="padding: 8px 0; font-size: 11px; font-weight: 700; color: #6b7280; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.04em;">Nouveau statut</td>
6081
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(statusLabel))}</strong></td>
6043
6082
  </tr>
6044
6083
  </table>`,
6045
- contextMessage,
6046
6084
  emailButton("Consulter le ticket", ticketUrl)
6047
- ].join(""))
6085
+ ].join(""), {
6086
+ kind: "system",
6087
+ preheader: `Ticket ${ticketNumber} \u2014 nouveau statut : ${statusLabel}`
6088
+ })
6048
6089
  });
6049
6090
  payload.logger.info(`[tickets] Status change notification sent to ${client.email} for ${ticketNumber} (${previousDoc?.status} -> ${doc.status})`);
6050
6091
  } catch (error) {
@@ -6560,16 +6601,27 @@ function createNotifyClientOnResolve(slugs) {
6560
6601
  depth: 0,
6561
6602
  overrideAccess: true
6562
6603
  }) : null;
6563
- if (!clientData?.email || clientData.notifyOnStatusChange === false) return doc;
6604
+ if (!clientData?.email) return doc;
6605
+ if (clientData.notifyResolution === false || clientData.notifyOnStatusChange === false) return doc;
6564
6606
  const settings = await readSupportSettings(req.payload);
6565
6607
  const ticketNumber = doc.ticketNumber || "TK-????";
6608
+ const subject = doc.subject || "Support";
6566
6609
  const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
6567
6610
  const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
6611
+ const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`;
6568
6612
  await req.payload.sendEmail({
6569
6613
  to: clientData.email,
6570
6614
  ...replyTo ? { replyTo } : {},
6571
6615
  subject: `[${ticketNumber}] Ticket resolu`,
6572
- html: `<p>Bonjour ${escapeHtml(clientData.firstName || "")},</p><p>Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> a ete resolu.</p><p><a href="${baseUrl}/support/tickets/${doc.id}">Consulter le ticket</a></p>`
6616
+ html: emailWrapper(`Votre ticket a ete resolu`, [
6617
+ emailParagraph(`Bonjour <strong>${escapeHtml(clientData.firstName || "")}</strong>,`),
6618
+ emailParagraph(`Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(String(subject))}</em> \u2014 a ete resolu.`),
6619
+ emailParagraph("Si le probleme persiste ou si vous avez une question, repondez simplement a cet email pour rouvrir le ticket."),
6620
+ emailButton("Consulter le ticket", ticketUrl)
6621
+ ].join(""), {
6622
+ kind: "system",
6623
+ preheader: `Votre ticket ${ticketNumber} a ete resolu.`
6624
+ })
6573
6625
  });
6574
6626
  } catch (err) {
6575
6627
  console.error("[support] Failed to notify client on resolve:", err);
@@ -7166,10 +7218,13 @@ function createNotifyClient(slugs) {
7166
7218
  const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
7167
7219
  const supportEmail = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
7168
7220
  const portalUrl = `${baseUrl}/support/tickets/${ticketId}`;
7221
+ const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
7169
7222
  const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
7223
+ const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
7170
7224
  await req.payload.sendEmail({
7171
7225
  to: client.email,
7172
7226
  ...supportEmail ? { replyTo: supportEmail } : {},
7227
+ ...internalBcc ? { bcc: internalBcc } : {},
7173
7228
  subject: `Re: [${ticketNumber}] ${subject}`,
7174
7229
  html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
7175
7230
  emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
@@ -7178,7 +7233,10 @@ function createNotifyClient(slugs) {
7178
7233
  emailButton("Consulter le ticket", portalUrl),
7179
7234
  emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
7180
7235
  emailTrackingPixel(ticketId, doc.id)
7181
- ].join(""))
7236
+ ].join(""), {
7237
+ kind: "reply",
7238
+ preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
7239
+ })
7182
7240
  });
7183
7241
  await req.payload.update({
7184
7242
  collection: slugs.ticketMessages,
@@ -7288,6 +7346,10 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
7288
7346
  const headerTitle = isNewTicket ? `Nouveau ticket ${ticketNumber}` : `Nouveau message \u2014 ${ticketNumber}`;
7289
7347
  if (primaryEmail) {
7290
7348
  await payload.sendEmail({
7349
+ // INTERNAL email (to the support team) — recipients are the contact
7350
+ // inbox + assigned agent. The cc here is internal→internal and stays
7351
+ // a cc on purpose. Do NOT confuse with the client reply email
7352
+ // (createNotifyClient), where internal copies must be bcc.
7291
7353
  to: primaryEmail,
7292
7354
  ...ccEmail ? { cc: ccEmail } : {},
7293
7355
  ...clientEmail !== "inconnu" ? { replyTo: clientEmail } : supportEmail ? { replyTo: supportEmail } : {},
@@ -7295,9 +7357,9 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
7295
7357
  html: emailWrapper(headerTitle, [
7296
7358
  emailParagraph(`<strong>${escapeHtml(clientName)}</strong> (${escapeHtml(clientEmail)}) a ${isNewTicket ? "ouvert un nouveau ticket" : "repondu au ticket"} <strong>${escapeHtml(ticketNumber)}</strong> :`),
7297
7359
  `<p style="margin: 0 0 4px 0; font-size: 14px; font-weight: 600; color: #374151;">Sujet : ${escapeHtml(subject)}</p>`,
7298
- emailQuote(preview, isNewTicket ? "#FFD600" : "#00E5FF"),
7360
+ emailQuote(preview, isNewTicket ? "#1d2b4d" : "#17807c"),
7299
7361
  emailButton("Ouvrir dans l'admin", adminUrl, "dark")
7300
- ].join(""), { headerColor: isNewTicket ? "secondary" : "primary" })
7362
+ ].join(""), { kind: "system" })
7301
7363
  });
7302
7364
  }
7303
7365
  console.log(`[support] Admin notified for ${ticketNumber} (${isNewTicket ? "new" : "reply"})`);
@@ -7556,6 +7618,79 @@ function createSupportClientsCollection(slugs) {
7556
7618
  }
7557
7619
  ]
7558
7620
  },
7621
+ {
7622
+ type: "row",
7623
+ fields: [
7624
+ {
7625
+ name: "city",
7626
+ type: "text",
7627
+ label: "Ville",
7628
+ admin: { width: "50%" }
7629
+ },
7630
+ {
7631
+ name: "country",
7632
+ type: "text",
7633
+ label: "Pays",
7634
+ admin: { width: "50%" }
7635
+ }
7636
+ ]
7637
+ },
7638
+ {
7639
+ name: "additionalContacts",
7640
+ type: "array",
7641
+ label: "Contacts additionnels",
7642
+ labels: {
7643
+ singular: "Contact",
7644
+ plural: "Contacts"
7645
+ },
7646
+ fields: [
7647
+ {
7648
+ type: "row",
7649
+ fields: [
7650
+ {
7651
+ name: "email",
7652
+ type: "email",
7653
+ required: true,
7654
+ label: "Email",
7655
+ admin: { width: "50%" }
7656
+ },
7657
+ {
7658
+ name: "name",
7659
+ type: "text",
7660
+ label: "Nom",
7661
+ admin: { width: "50%" }
7662
+ }
7663
+ ]
7664
+ }
7665
+ ]
7666
+ },
7667
+ {
7668
+ name: "opportunities",
7669
+ type: "textarea",
7670
+ label: "Opportunites commerciales",
7671
+ admin: {
7672
+ description: "Notes commerciales (visible uniquement par les admins)"
7673
+ }
7674
+ },
7675
+ {
7676
+ name: "tier",
7677
+ type: "select",
7678
+ label: "Plan",
7679
+ options: [
7680
+ { label: "Free", value: "free" },
7681
+ { label: "Basic", value: "basic" },
7682
+ { label: "Pro", value: "pro" },
7683
+ { label: "Enterprise", value: "enterprise" }
7684
+ ],
7685
+ admin: { position: "sidebar" }
7686
+ },
7687
+ {
7688
+ name: "accountManager",
7689
+ type: "relationship",
7690
+ relationTo: slugs.users,
7691
+ label: "Agent referent",
7692
+ admin: { position: "sidebar" }
7693
+ },
7559
7694
  {
7560
7695
  name: "twoFactorEnabled",
7561
7696
  type: "checkbox",
@@ -7596,6 +7731,48 @@ function createSupportClientsCollection(slugs) {
7596
7731
  position: "sidebar"
7597
7732
  }
7598
7733
  },
7734
+ {
7735
+ name: "notifyReminders",
7736
+ type: "checkbox",
7737
+ defaultValue: true,
7738
+ label: "Notifications rappels",
7739
+ admin: {
7740
+ description: "Recevoir un email pour les rappels",
7741
+ position: "sidebar"
7742
+ }
7743
+ },
7744
+ {
7745
+ name: "notifyResolution",
7746
+ type: "checkbox",
7747
+ defaultValue: true,
7748
+ label: "Notifications resolution",
7749
+ admin: {
7750
+ description: "Recevoir un email a la resolution d'un ticket",
7751
+ position: "sidebar"
7752
+ }
7753
+ },
7754
+ {
7755
+ name: "notifyNews",
7756
+ type: "checkbox",
7757
+ defaultValue: false,
7758
+ label: "Notifications actualites",
7759
+ admin: {
7760
+ description: "Recevoir les actualites et nouveautes",
7761
+ position: "sidebar"
7762
+ }
7763
+ },
7764
+ {
7765
+ name: "notificationFrequency",
7766
+ type: "select",
7767
+ defaultValue: "immediate",
7768
+ label: "Frequence des notifications",
7769
+ options: [
7770
+ { label: "Immediate", value: "immediate" },
7771
+ { label: "Quotidienne", value: "daily" },
7772
+ { label: "Hebdomadaire", value: "weekly" }
7773
+ ],
7774
+ admin: { position: "sidebar" }
7775
+ },
7599
7776
  {
7600
7777
  name: "notes",
7601
7778
  type: "textarea",
@@ -7708,6 +7885,18 @@ function createTimeEntriesCollection(slugs) {
7708
7885
  name: "description",
7709
7886
  type: "textarea",
7710
7887
  label: "Description du travail"
7888
+ },
7889
+ {
7890
+ name: "billable",
7891
+ type: "checkbox",
7892
+ defaultValue: true,
7893
+ label: "Facturable"
7894
+ },
7895
+ {
7896
+ name: "agent",
7897
+ type: "relationship",
7898
+ relationTo: slugs.users,
7899
+ label: "Agent"
7711
7900
  }
7712
7901
  ],
7713
7902
  hooks: {
@@ -8008,6 +8197,14 @@ function createKnowledgeBaseCollection(slugs) {
8008
8197
  { label: "G\xE9n\xE9ral", value: "general" }
8009
8198
  ]
8010
8199
  },
8200
+ {
8201
+ name: "excerpt",
8202
+ type: "textarea",
8203
+ label: "R\xE9sum\xE9",
8204
+ admin: {
8205
+ description: "Court r\xE9sum\xE9 affich\xE9 dans les listes"
8206
+ }
8207
+ },
8011
8208
  {
8012
8209
  name: "body",
8013
8210
  type: "richText",
@@ -8021,12 +8218,33 @@ function createKnowledgeBaseCollection(slugs) {
8021
8218
  label: "Publi\xE9",
8022
8219
  admin: { position: "sidebar" }
8023
8220
  },
8221
+ {
8222
+ name: "popular",
8223
+ type: "checkbox",
8224
+ defaultValue: false,
8225
+ label: "Populaire",
8226
+ admin: { position: "sidebar" }
8227
+ },
8024
8228
  {
8025
8229
  name: "sortOrder",
8026
8230
  type: "number",
8027
8231
  defaultValue: 0,
8028
8232
  label: "Ordre",
8029
8233
  admin: { position: "sidebar" }
8234
+ },
8235
+ {
8236
+ name: "helpfulYes",
8237
+ type: "number",
8238
+ defaultValue: 0,
8239
+ label: "Votes utiles",
8240
+ admin: { position: "sidebar", readOnly: true }
8241
+ },
8242
+ {
8243
+ name: "helpfulNo",
8244
+ type: "number",
8245
+ defaultValue: 0,
8246
+ label: "Votes non utiles",
8247
+ admin: { position: "sidebar", readOnly: true }
8030
8248
  }
8031
8249
  ],
8032
8250
  hooks: {