@consilioweb/payload-support 0.13.0 → 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.js CHANGED
@@ -1519,17 +1519,31 @@ function createSlaCheckEndpoint(slugs) {
1519
1519
  }
1520
1520
 
1521
1521
  // src/utils/emailTemplate.ts
1522
+ var COLORS = {
1523
+ ink: "#1d2b4d",
1524
+ inkSoft: "#3a486a",
1525
+ teal: "#17807c",
1526
+ amber: "#c2410c",
1527
+ paper: "#faf8f5",
1528
+ card: "#ffffff",
1529
+ sand: "#f4f1ec",
1530
+ line: "#e7e2d9",
1531
+ text: "#2a3142",
1532
+ muted: "#6b7280",
1533
+ faint: "#9aa1ad"
1534
+ };
1522
1535
  var DEFAULT_CONFIG = {
1523
1536
  brandName: "Support",
1524
- brandColor: "#00E5FF",
1525
- secondaryColor: "#FFD600",
1526
- accentColor: "#FF8A00",
1537
+ brandColor: COLORS.teal,
1538
+ secondaryColor: COLORS.ink,
1539
+ accentColor: COLORS.amber,
1527
1540
  logoUrl: "",
1528
1541
  supportEmail: process.env.SUPPORT_EMAIL || "",
1529
1542
  websiteUrl: process.env.NEXT_PUBLIC_SERVER_URL || "",
1530
1543
  phone: "",
1531
1544
  location: "",
1532
- brandInitials: ""
1545
+ brandInitials: "",
1546
+ preferencesUrl: ""
1533
1547
  };
1534
1548
  function resolveConfig(config) {
1535
1549
  return { ...DEFAULT_CONFIG, ...config };
@@ -1568,30 +1582,30 @@ function emailRichContent(html, config) {
1568
1582
  }
1569
1583
  let result = html.replace(/src="\/([^"]+)"/g, `src="${baseUrl}/$1"`);
1570
1584
  result = convertFencedCodeBlocks(result);
1571
- result = styleTag(result, "blockquote", `border-left: 4px solid ${c.brandColor}; margin: 16px 0; padding: 12px 20px; background: #f0f9fa; border-radius: 0 8px 8px 0;`);
1585
+ 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};`);
1572
1586
  result = styleTag(result, "img", "max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; display: block;");
1573
- result = styleTag(result, "a", `color: ${c.brandColor}; text-decoration: underline; font-weight: 600;`);
1587
+ result = styleTag(result, "a", `color: ${COLORS.teal}; text-decoration: underline; font-weight: 600;`);
1574
1588
  result = styleTag(result, "ul", "margin: 8px 0; padding-left: 24px;");
1575
1589
  result = styleTag(result, "ol", "margin: 8px 0; padding-left: 24px;");
1576
- result = styleTag(result, "li", "margin: 4px 0; line-height: 1.6;");
1577
- result = styleTag(result, "p", "margin: 0 0 12px 0; line-height: 1.75; font-size: 15px; color: #1f2937;");
1578
- 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, "");
1579
- return `<div style="font-size: 15px; line-height: 1.75; color: #1f2937;">${result}</div>`;
1590
+ result = styleTag(result, "li", `margin: 4px 0; line-height: 1.6; color: ${COLORS.text};`);
1591
+ result = styleTag(result, "p", `margin: 0 0 12px 0; line-height: 1.75; font-size: 15px; color: ${COLORS.text};`);
1592
+ 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, "");
1593
+ return `<div style="font-size: 15px; line-height: 1.75; color: ${COLORS.text};">${result}</div>`;
1580
1594
  }
1581
- function getButtonColors(config) {
1595
+ function getButtonColors(_config) {
1582
1596
  return {
1583
- primary: { bg: config.brandColor, text: "#000000", border: "#000000" },
1584
- secondary: { bg: config.secondaryColor, text: "#000000", border: "#000000" },
1585
- dark: { bg: "#000000", text: "#FFFFFF", border: "#000000" }
1597
+ primary: { bg: COLORS.teal, text: "#ffffff", border: COLORS.teal },
1598
+ secondary: { bg: COLORS.sand, text: COLORS.ink, border: COLORS.line },
1599
+ dark: { bg: COLORS.ink, text: "#ffffff", border: COLORS.ink }
1586
1600
  };
1587
1601
  }
1588
1602
  function emailButton(text, url, color = "primary", config) {
1589
- const c = resolveConfig(config);
1590
- const colors = getButtonColors(c);
1603
+ resolveConfig(config);
1604
+ const colors = getButtonColors();
1591
1605
  const bc = colors[color];
1592
1606
  return `
1593
1607
  <div style="text-align: center; margin: 32px 0;">
1594
- <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;">
1608
+ <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;">
1595
1609
  ${text}
1596
1610
  </a>
1597
1611
  </div>
@@ -1606,8 +1620,9 @@ function renderCodeBlockHtml(lang, code) {
1606
1620
  </div>`;
1607
1621
  }
1608
1622
  function emailQuote(content, borderColor, config) {
1609
- const c = resolveConfig(config);
1610
- const color = borderColor || c.brandColor;
1623
+ resolveConfig(config);
1624
+ const color = borderColor || COLORS.teal;
1625
+ const panel = `margin: 24px 0; padding: 18px 22px; background: ${COLORS.sand}; border-left: 3px solid ${color}; border-radius: 0 8px 8px 0;`;
1611
1626
  if (content && content.includes("```")) {
1612
1627
  const parts = [];
1613
1628
  const regex = /```(\w*)\n?([\s\S]*?)```/g;
@@ -1616,62 +1631,77 @@ function emailQuote(content, borderColor, config) {
1616
1631
  while ((match = regex.exec(content)) !== null) {
1617
1632
  const textBefore = content.slice(lastIndex, match.index).trim();
1618
1633
  if (textBefore) {
1619
- 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>`);
1634
+ 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>`);
1620
1635
  }
1621
1636
  parts.push(renderCodeBlockHtml(match[1] || "", match[2] || ""));
1622
1637
  lastIndex = match.index + match[0].length;
1623
1638
  }
1624
1639
  const textAfter = content.slice(lastIndex).trim();
1625
1640
  if (textAfter) {
1626
- 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>`);
1641
+ 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>`);
1627
1642
  }
1628
1643
  return `
1629
- <div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
1644
+ <div style="${panel}">
1630
1645
  ${parts.join("")}
1631
1646
  </div>
1632
1647
  `;
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;">
1636
- <p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
1650
+ <div style="${panel}">
1651
+ <p style="margin: 0; font-size: 15px; line-height: 1.75; color: ${COLORS.inkSoft}; white-space: pre-wrap;">${escapeHtml(content)}</p>
1637
1652
  </div>
1638
1653
  `;
1639
1654
  }
1640
1655
  function emailParagraph(text) {
1641
- return `<p style="margin: 0 0 18px 0; font-size: 15px; line-height: 1.75; color: #1f2937;">${text}</p>`;
1656
+ return `<p style="margin: 0 0 18px 0; font-size: 15px; line-height: 1.75; color: ${COLORS.text};">${text}</p>`;
1657
+ }
1658
+ function getKindStyle(kind) {
1659
+ switch (kind) {
1660
+ case "reply":
1661
+ return { eyebrow: "R\xE9ponse de votre \xE9quipe", accent: COLORS.teal };
1662
+ case "alert":
1663
+ return { eyebrow: "Action requise", accent: COLORS.amber };
1664
+ case "system":
1665
+ default:
1666
+ return { eyebrow: "Notification automatique", accent: COLORS.inkSoft };
1667
+ }
1642
1668
  }
1643
1669
  function emailFooter(config) {
1644
1670
  const logoHtml = config.logoUrl ? `<a href="${config.websiteUrl}">
1645
1671
  <img src="${config.logoUrl}" alt="${escapeHtml(config.brandName)}" width="100" height="47" style="display: block; border: 0;" />
1646
- </a>` : `<a href="${config.websiteUrl}" style="font-size: 18px; font-weight: 900; color: #000000; text-decoration: none;">${escapeHtml(config.brandName)}</a>`;
1672
+ </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>`;
1647
1673
  const contactParts = [];
1648
1674
  if (config.supportEmail) {
1649
- contactParts.push(`<a href="mailto:${config.supportEmail}" style="color: #555555; text-decoration: none;">${config.supportEmail}</a>`);
1675
+ contactParts.push(`<a href="mailto:${config.supportEmail}" style="color: ${COLORS.muted}; text-decoration: none;">${config.supportEmail}</a>`);
1650
1676
  }
1651
1677
  if (config.phone) {
1652
- contactParts.push(`<a href="tel:${config.phone.replace(/\s/g, "")}" style="color: #555555; text-decoration: none;">${config.phone}</a>`);
1678
+ contactParts.push(`<a href="tel:${config.phone.replace(/\s/g, "")}" style="color: ${COLORS.muted}; text-decoration: none;">${config.phone}</a>`);
1653
1679
  }
1654
1680
  const locationParts = [];
1655
1681
  if (config.websiteUrl) {
1656
1682
  const displayUrl = config.websiteUrl.replace(/^https?:\/\//, "");
1657
- locationParts.push(`<a href="${config.websiteUrl}" style="color: ${config.brandColor}; text-decoration: none; font-weight: 600;">${displayUrl}</a>`);
1683
+ locationParts.push(`<a href="${config.websiteUrl}" style="color: ${COLORS.teal}; text-decoration: none; font-weight: 600;">${displayUrl}</a>`);
1658
1684
  }
1659
1685
  if (config.location) {
1660
1686
  locationParts.push(config.location);
1661
1687
  }
1662
- const unsubscribeHtml = config.supportEmail ? `<p style="margin: 8px 0 0 0; font-size: 11px; color: #aaaaaa; line-height: 1.4;">
1663
- <a href="mailto:${config.supportEmail}?subject=Unsubscribe" style="color: #aaaaaa; text-decoration: underline;">Se d&eacute;sinscrire</a>
1664
- </p>` : "";
1688
+ const manageUrl = config.preferencesUrl || (config.websiteUrl ? `${config.websiteUrl}/support/profile` : "") || (config.supportEmail ? `mailto:${config.supportEmail}?subject=Notifications` : "");
1689
+ const preferencesLinks = [];
1690
+ if (manageUrl) {
1691
+ preferencesLinks.push(`<a href="${manageUrl}" style="color: ${COLORS.muted}; text-decoration: underline;">G\xE9rer mes notifications</a>`);
1692
+ }
1693
+ if (config.supportEmail) {
1694
+ preferencesLinks.push(`<a href="mailto:${config.supportEmail}?subject=Unsubscribe" style="color: ${COLORS.faint}; text-decoration: underline;">Se d\xE9sinscrire</a>`);
1695
+ }
1696
+ 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>` : "";
1665
1697
  return `
1666
1698
  <!-- Spacer -->
1667
- <div style="height: 24px;"></div>
1699
+ <div style="height: 28px;"></div>
1668
1700
 
1669
- <!-- Tricolor separator -->
1670
- <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 24px;">
1701
+ <!-- Hairline separator -->
1702
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 22px;">
1671
1703
  <tr>
1672
- <td width="50%" height="3" bgcolor="${config.brandColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1673
- <td width="25%" height="3" bgcolor="${config.secondaryColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1674
- <td width="25%" height="3" bgcolor="${config.accentColor}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1704
+ <td height="1" bgcolor="${COLORS.line}" style="font-size:1px;line-height:1px;">&nbsp;</td>
1675
1705
  </tr>
1676
1706
  </table>
1677
1707
 
@@ -1682,10 +1712,10 @@ function emailFooter(config) {
1682
1712
  ${logoHtml}
1683
1713
  </td>
1684
1714
  <td valign="top">
1685
- <p style="margin: 0; font-size: 14px; font-weight: 800; color: #000000; letter-spacing: 0.01em;">${escapeHtml(config.brandName)}</p>
1686
- ${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>` : ""}
1687
- ${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>` : ""}
1688
- ${unsubscribeHtml}
1715
+ <p style="margin: 0; font-size: 13px; font-weight: 700; color: ${COLORS.ink}; letter-spacing: 0.01em;">${escapeHtml(config.brandName)}</p>
1716
+ ${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>` : ""}
1717
+ ${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>` : ""}
1718
+ ${preferencesHtml}
1689
1719
  </td>
1690
1720
  </tr>
1691
1721
  </table>
@@ -1693,11 +1723,13 @@ function emailFooter(config) {
1693
1723
  }
1694
1724
  function emailWrapper(title, body, options = {}, config) {
1695
1725
  const c = resolveConfig(config);
1696
- const { headerColor = "primary", preheader } = options;
1697
- const headerBg = headerColor === "secondary" ? c.secondaryColor : c.brandColor;
1726
+ const { preheader, kind } = options;
1727
+ const kindStyle = kind ? getKindStyle(kind) : null;
1728
+ const accent = kindStyle?.accent || COLORS.teal;
1729
+ 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>` : "";
1698
1730
  const badgeHtml = c.brandInitials ? `<td width="48" align="right" valign="middle">
1699
- <div style="width: 36px; height: 36px; border-radius: 8px; background: #000; display: inline-block; text-align: center; line-height: 36px;">
1700
- <span style="color: #fff; font-weight: 900; font-size: 14px; letter-spacing: 0.05em;">${escapeHtml(c.brandInitials)}</span>
1731
+ <div style="width: 38px; height: 38px; border-radius: 10px; background: ${COLORS.ink}; display: inline-block; text-align: center; line-height: 38px;">
1732
+ <span style="color: #fff; font-weight: 800; font-size: 14px; letter-spacing: 0.04em;">${escapeHtml(c.brandInitials)}</span>
1701
1733
  </div>
1702
1734
  </td>` : "";
1703
1735
  return `<!DOCTYPE html>
@@ -1712,23 +1744,29 @@ function emailWrapper(title, body, options = {}, config) {
1712
1744
  </style>
1713
1745
  <![endif]-->
1714
1746
  </head>
1715
- <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;">
1716
- ${preheader ? `<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">${escapeHtml(preheader)}</div>` : ""}
1747
+ <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;">
1748
+ ${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>` : ""}
1717
1749
 
1718
1750
  <!-- Outer wrapper -->
1719
- <table cellpadding="0" cellspacing="0" border="0" width="100%" style="background: #f0f0f0; padding: 40px 16px;">
1751
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="background: ${COLORS.paper}; padding: 40px 16px;">
1720
1752
  <tr>
1721
1753
  <td align="center">
1722
- <!-- Main container \u2014 wider (660px) -->
1723
- <table cellpadding="0" cellspacing="0" border="0" width="660" style="max-width: 660px; width: 100%;">
1754
+ <!-- Main container \u2014 640px -->
1755
+ <table cellpadding="0" cellspacing="0" border="0" width="640" style="max-width: 640px; width: 100%;">
1756
+
1757
+ <!-- Accent rule -->
1758
+ <tr>
1759
+ <td style="height: 4px; background: ${accent}; border-radius: 12px 12px 0 0; font-size: 1px; line-height: 1px;">&nbsp;</td>
1760
+ </tr>
1724
1761
 
1725
1762
  <!-- Header -->
1726
1763
  <tr>
1727
- <td style="background: ${headerBg}; padding: 32px 40px; border-radius: 12px 12px 0 0; border: 2px solid #000000; border-bottom: none;">
1764
+ <td style="background: ${COLORS.card}; padding: 30px 40px 22px 40px; border: 1px solid ${COLORS.line}; border-top: none; border-bottom: none;">
1728
1765
  <table cellpadding="0" cellspacing="0" border="0" width="100%">
1729
1766
  <tr>
1730
- <td>
1731
- <h1 style="margin: 0; color: #000000; font-size: 22px; font-weight: 800; line-height: 1.3; letter-spacing: -0.01em;">
1767
+ <td valign="middle">
1768
+ ${eyebrowHtml}
1769
+ <h1 style="margin: 0; color: ${COLORS.ink}; font-size: 21px; font-weight: 800; line-height: 1.3; letter-spacing: -0.01em;">
1732
1770
  ${escapeHtml(title)}
1733
1771
  </h1>
1734
1772
  </td>
@@ -1740,7 +1778,7 @@ function emailWrapper(title, body, options = {}, config) {
1740
1778
 
1741
1779
  <!-- Body -->
1742
1780
  <tr>
1743
- <td style="background: #ffffff; padding: 40px; border: 2px solid #000000; border-top: none; border-radius: 0 0 12px 12px;">
1781
+ <td style="background: ${COLORS.card}; padding: 8px 40px 40px 40px; border: 1px solid ${COLORS.line}; border-top: none; border-radius: 0 0 12px 12px;">
1744
1782
  ${body}
1745
1783
  ${emailFooter(c)}
1746
1784
  </td>
@@ -5919,8 +5957,12 @@ async function _sendToEndpoint(endpoint, body, payload, slugs) {
5919
5957
  var STATUS_LABELS = {
5920
5958
  open: "Ouvert",
5921
5959
  waiting_client: "En attente client",
5922
- resolved: "Resolu"
5960
+ waiting_support: "En attente support",
5961
+ resolved: "Resolu",
5962
+ closed: "Ferme",
5963
+ escalated: "Escalade"
5923
5964
  };
5965
+ var SILENT_STATUSES = /* @__PURE__ */ new Set(["waiting_client", "waiting_support", "resolved"]);
5924
5966
  async function resolveClient(doc, payload, clientSlug) {
5925
5967
  const client = doc.client;
5926
5968
  if (!client) return null;
@@ -5972,21 +6014,24 @@ function createTicketStatusEmail(slugs) {
5972
6014
  emailParagraph(`Nous avons bien recu votre demande de support. Voici les details :`),
5973
6015
  `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
5974
6016
  <tr>
5975
- <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>
5976
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6017
+ <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>
6018
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
5977
6019
  </tr>
5978
6020
  <tr>
5979
- <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>
5980
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6021
+ <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>
6022
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
5981
6023
  </tr>
5982
6024
  <tr>
5983
- <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>
5984
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${STATUS_LABELS[doc.status] || doc.status}</td>
6025
+ <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>
6026
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(STATUS_LABELS[doc.status] || String(doc.status))}</td>
5985
6027
  </tr>
5986
6028
  </table>`,
5987
6029
  emailParagraph("Notre equipe vous repondra dans les meilleurs delais."),
5988
6030
  emailButton("Consulter le ticket", ticketUrl)
5989
- ].join(""))
6031
+ ].join(""), {
6032
+ kind: "system",
6033
+ preheader: `Votre demande ${ticketNumber} est bien enregistree. Nous revenons vers vous rapidement.`
6034
+ })
5990
6035
  });
5991
6036
  payload.logger.info(`[tickets] Creation notification sent to ${client.email} for ${ticketNumber}`);
5992
6037
  } catch (error) {
@@ -5995,7 +6040,7 @@ function createTicketStatusEmail(slugs) {
5995
6040
  return doc;
5996
6041
  }
5997
6042
  if (operation === "update" && previousDoc?.status !== doc.status) {
5998
- if (doc.status === "resolved") return doc;
6043
+ if (SILENT_STATUSES.has(doc.status)) return doc;
5999
6044
  try {
6000
6045
  const client = await resolveClient(doc, payload, slugs.supportClients);
6001
6046
  if (!client?.email) return doc;
@@ -6005,12 +6050,6 @@ function createTicketStatusEmail(slugs) {
6005
6050
  const statusLabel = STATUS_LABELS[doc.status] || doc.status;
6006
6051
  const previousStatusLabel = STATUS_LABELS[previousDoc?.status] || previousDoc?.status || "N/A";
6007
6052
  const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`;
6008
- let contextMessage = "";
6009
- if (doc.status === "waiting_client") {
6010
- contextMessage = emailParagraph(
6011
- "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."
6012
- );
6013
- }
6014
6053
  await payload.sendEmail({
6015
6054
  to: client.email,
6016
6055
  ...supportEmail ? { replyTo: supportEmail } : {},
@@ -6020,25 +6059,27 @@ function createTicketStatusEmail(slugs) {
6020
6059
  emailParagraph(`Le statut de votre ticket a ete mis a jour :`),
6021
6060
  `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 20px;">
6022
6061
  <tr>
6023
- <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>
6024
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6062
+ <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>
6063
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(ticketNumber))}</strong></td>
6025
6064
  </tr>
6026
6065
  <tr>
6027
- <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>
6028
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6066
+ <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>
6067
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(subject))}</td>
6029
6068
  </tr>
6030
6069
  <tr>
6031
- <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>
6032
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${escapeHtml(String(previousStatusLabel))}</td>
6070
+ <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>
6071
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;">${escapeHtml(String(previousStatusLabel))}</td>
6033
6072
  </tr>
6034
6073
  <tr>
6035
- <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>
6036
- <td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;"><strong>${escapeHtml(String(statusLabel))}</strong></td>
6074
+ <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>
6075
+ <td style="padding: 8px 0; font-size: 15px; color: #2a3142; line-height: 1.5;"><strong>${escapeHtml(String(statusLabel))}</strong></td>
6037
6076
  </tr>
6038
6077
  </table>`,
6039
- contextMessage,
6040
6078
  emailButton("Consulter le ticket", ticketUrl)
6041
- ].join(""))
6079
+ ].join(""), {
6080
+ kind: "system",
6081
+ preheader: `Ticket ${ticketNumber} \u2014 nouveau statut : ${statusLabel}`
6082
+ })
6042
6083
  });
6043
6084
  payload.logger.info(`[tickets] Status change notification sent to ${client.email} for ${ticketNumber} (${previousDoc?.status} -> ${doc.status})`);
6044
6085
  } catch (error) {
@@ -6554,16 +6595,27 @@ function createNotifyClientOnResolve(slugs) {
6554
6595
  depth: 0,
6555
6596
  overrideAccess: true
6556
6597
  }) : null;
6557
- if (!clientData?.email || clientData.notifyOnStatusChange === false) return doc;
6598
+ if (!clientData?.email) return doc;
6599
+ if (clientData.notifyResolution === false || clientData.notifyOnStatusChange === false) return doc;
6558
6600
  const settings = await readSupportSettings(req.payload);
6559
6601
  const ticketNumber = doc.ticketNumber || "TK-????";
6602
+ const subject = doc.subject || "Support";
6560
6603
  const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
6561
6604
  const replyTo = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
6605
+ const ticketUrl = `${baseUrl}/support/tickets/${doc.id}`;
6562
6606
  await req.payload.sendEmail({
6563
6607
  to: clientData.email,
6564
6608
  ...replyTo ? { replyTo } : {},
6565
6609
  subject: `[${ticketNumber}] Ticket resolu`,
6566
- 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>`
6610
+ html: emailWrapper(`Votre ticket a ete resolu`, [
6611
+ emailParagraph(`Bonjour <strong>${escapeHtml(clientData.firstName || "")}</strong>,`),
6612
+ emailParagraph(`Votre ticket <strong>${escapeHtml(ticketNumber)}</strong> \u2014 <em>${escapeHtml(String(subject))}</em> \u2014 a ete resolu.`),
6613
+ emailParagraph("Si le probleme persiste ou si vous avez une question, repondez simplement a cet email pour rouvrir le ticket."),
6614
+ emailButton("Consulter le ticket", ticketUrl)
6615
+ ].join(""), {
6616
+ kind: "system",
6617
+ preheader: `Votre ticket ${ticketNumber} a ete resolu.`
6618
+ })
6567
6619
  });
6568
6620
  } catch (err) {
6569
6621
  console.error("[support] Failed to notify client on resolve:", err);
@@ -7160,10 +7212,13 @@ function createNotifyClient(slugs) {
7160
7212
  const baseUrl = process.env.NEXT_PUBLIC_SERVER_URL || "";
7161
7213
  const supportEmail = settings.email.replyToAddress || process.env.SUPPORT_EMAIL || "";
7162
7214
  const portalUrl = `${baseUrl}/support/tickets/${ticketId}`;
7215
+ const internalBcc = process.env.SUPPORT_CLIENT_BCC || void 0;
7163
7216
  const rawContent = doc.bodyHtml ? emailRichContent(doc.bodyHtml) : emailQuote(doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body);
7217
+ const replyPreview = (doc.body || "").replace(/\s+/g, " ").trim().slice(0, 90);
7164
7218
  await req.payload.sendEmail({
7165
7219
  to: client.email,
7166
7220
  ...supportEmail ? { replyTo: supportEmail } : {},
7221
+ ...internalBcc ? { bcc: internalBcc } : {},
7167
7222
  subject: `Re: [${ticketNumber}] ${subject}`,
7168
7223
  html: emailWrapper(`Nouvelle reponse \u2014 ${ticketNumber}`, [
7169
7224
  emailParagraph(`Bonjour <strong>${escapeHtml(client.firstName || "")}</strong>,`),
@@ -7172,7 +7227,10 @@ function createNotifyClient(slugs) {
7172
7227
  emailButton("Consulter le ticket", portalUrl),
7173
7228
  emailParagraph('<span style="font-size: 13px; color: #6b7280;">Vous pouvez egalement repondre directement a cet email. Votre message sera automatiquement ajoute au ticket.</span>'),
7174
7229
  emailTrackingPixel(ticketId, doc.id)
7175
- ].join(""))
7230
+ ].join(""), {
7231
+ kind: "reply",
7232
+ preheader: replyPreview ? `${replyPreview}\u2026` : `Nouvelle reponse a votre ticket ${ticketNumber}`
7233
+ })
7176
7234
  });
7177
7235
  await req.payload.update({
7178
7236
  collection: slugs.ticketMessages,
@@ -7282,6 +7340,10 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
7282
7340
  const headerTitle = isNewTicket ? `Nouveau ticket ${ticketNumber}` : `Nouveau message \u2014 ${ticketNumber}`;
7283
7341
  if (primaryEmail) {
7284
7342
  await payload.sendEmail({
7343
+ // INTERNAL email (to the support team) — recipients are the contact
7344
+ // inbox + assigned agent. The cc here is internal→internal and stays
7345
+ // a cc on purpose. Do NOT confuse with the client reply email
7346
+ // (createNotifyClient), where internal copies must be bcc.
7285
7347
  to: primaryEmail,
7286
7348
  ...ccEmail ? { cc: ccEmail } : {},
7287
7349
  ...clientEmail !== "inconnu" ? { replyTo: clientEmail } : supportEmail ? { replyTo: supportEmail } : {},
@@ -7289,9 +7351,9 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
7289
7351
  html: emailWrapper(headerTitle, [
7290
7352
  emailParagraph(`<strong>${escapeHtml(clientName)}</strong> (${escapeHtml(clientEmail)}) a ${isNewTicket ? "ouvert un nouveau ticket" : "repondu au ticket"} <strong>${escapeHtml(ticketNumber)}</strong> :`),
7291
7353
  `<p style="margin: 0 0 4px 0; font-size: 14px; font-weight: 600; color: #374151;">Sujet : ${escapeHtml(subject)}</p>`,
7292
- emailQuote(preview, isNewTicket ? "#FFD600" : "#00E5FF"),
7354
+ emailQuote(preview, isNewTicket ? "#1d2b4d" : "#17807c"),
7293
7355
  emailButton("Ouvrir dans l'admin", adminUrl, "dark")
7294
- ].join(""), { headerColor: isNewTicket ? "secondary" : "primary" })
7356
+ ].join(""), { kind: "system" })
7295
7357
  });
7296
7358
  }
7297
7359
  console.log(`[support] Admin notified for ${ticketNumber} (${isNewTicket ? "new" : "reply"})`);
@@ -7550,6 +7612,79 @@ function createSupportClientsCollection(slugs) {
7550
7612
  }
7551
7613
  ]
7552
7614
  },
7615
+ {
7616
+ type: "row",
7617
+ fields: [
7618
+ {
7619
+ name: "city",
7620
+ type: "text",
7621
+ label: "Ville",
7622
+ admin: { width: "50%" }
7623
+ },
7624
+ {
7625
+ name: "country",
7626
+ type: "text",
7627
+ label: "Pays",
7628
+ admin: { width: "50%" }
7629
+ }
7630
+ ]
7631
+ },
7632
+ {
7633
+ name: "additionalContacts",
7634
+ type: "array",
7635
+ label: "Contacts additionnels",
7636
+ labels: {
7637
+ singular: "Contact",
7638
+ plural: "Contacts"
7639
+ },
7640
+ fields: [
7641
+ {
7642
+ type: "row",
7643
+ fields: [
7644
+ {
7645
+ name: "email",
7646
+ type: "email",
7647
+ required: true,
7648
+ label: "Email",
7649
+ admin: { width: "50%" }
7650
+ },
7651
+ {
7652
+ name: "name",
7653
+ type: "text",
7654
+ label: "Nom",
7655
+ admin: { width: "50%" }
7656
+ }
7657
+ ]
7658
+ }
7659
+ ]
7660
+ },
7661
+ {
7662
+ name: "opportunities",
7663
+ type: "textarea",
7664
+ label: "Opportunites commerciales",
7665
+ admin: {
7666
+ description: "Notes commerciales (visible uniquement par les admins)"
7667
+ }
7668
+ },
7669
+ {
7670
+ name: "tier",
7671
+ type: "select",
7672
+ label: "Plan",
7673
+ options: [
7674
+ { label: "Free", value: "free" },
7675
+ { label: "Basic", value: "basic" },
7676
+ { label: "Pro", value: "pro" },
7677
+ { label: "Enterprise", value: "enterprise" }
7678
+ ],
7679
+ admin: { position: "sidebar" }
7680
+ },
7681
+ {
7682
+ name: "accountManager",
7683
+ type: "relationship",
7684
+ relationTo: slugs.users,
7685
+ label: "Agent referent",
7686
+ admin: { position: "sidebar" }
7687
+ },
7553
7688
  {
7554
7689
  name: "twoFactorEnabled",
7555
7690
  type: "checkbox",
@@ -7590,6 +7725,48 @@ function createSupportClientsCollection(slugs) {
7590
7725
  position: "sidebar"
7591
7726
  }
7592
7727
  },
7728
+ {
7729
+ name: "notifyReminders",
7730
+ type: "checkbox",
7731
+ defaultValue: true,
7732
+ label: "Notifications rappels",
7733
+ admin: {
7734
+ description: "Recevoir un email pour les rappels",
7735
+ position: "sidebar"
7736
+ }
7737
+ },
7738
+ {
7739
+ name: "notifyResolution",
7740
+ type: "checkbox",
7741
+ defaultValue: true,
7742
+ label: "Notifications resolution",
7743
+ admin: {
7744
+ description: "Recevoir un email a la resolution d'un ticket",
7745
+ position: "sidebar"
7746
+ }
7747
+ },
7748
+ {
7749
+ name: "notifyNews",
7750
+ type: "checkbox",
7751
+ defaultValue: false,
7752
+ label: "Notifications actualites",
7753
+ admin: {
7754
+ description: "Recevoir les actualites et nouveautes",
7755
+ position: "sidebar"
7756
+ }
7757
+ },
7758
+ {
7759
+ name: "notificationFrequency",
7760
+ type: "select",
7761
+ defaultValue: "immediate",
7762
+ label: "Frequence des notifications",
7763
+ options: [
7764
+ { label: "Immediate", value: "immediate" },
7765
+ { label: "Quotidienne", value: "daily" },
7766
+ { label: "Hebdomadaire", value: "weekly" }
7767
+ ],
7768
+ admin: { position: "sidebar" }
7769
+ },
7593
7770
  {
7594
7771
  name: "notes",
7595
7772
  type: "textarea",
@@ -7702,6 +7879,18 @@ function createTimeEntriesCollection(slugs) {
7702
7879
  name: "description",
7703
7880
  type: "textarea",
7704
7881
  label: "Description du travail"
7882
+ },
7883
+ {
7884
+ name: "billable",
7885
+ type: "checkbox",
7886
+ defaultValue: true,
7887
+ label: "Facturable"
7888
+ },
7889
+ {
7890
+ name: "agent",
7891
+ type: "relationship",
7892
+ relationTo: slugs.users,
7893
+ label: "Agent"
7705
7894
  }
7706
7895
  ],
7707
7896
  hooks: {
@@ -8002,6 +8191,14 @@ function createKnowledgeBaseCollection(slugs) {
8002
8191
  { label: "G\xE9n\xE9ral", value: "general" }
8003
8192
  ]
8004
8193
  },
8194
+ {
8195
+ name: "excerpt",
8196
+ type: "textarea",
8197
+ label: "R\xE9sum\xE9",
8198
+ admin: {
8199
+ description: "Court r\xE9sum\xE9 affich\xE9 dans les listes"
8200
+ }
8201
+ },
8005
8202
  {
8006
8203
  name: "body",
8007
8204
  type: "richText",
@@ -8015,12 +8212,33 @@ function createKnowledgeBaseCollection(slugs) {
8015
8212
  label: "Publi\xE9",
8016
8213
  admin: { position: "sidebar" }
8017
8214
  },
8215
+ {
8216
+ name: "popular",
8217
+ type: "checkbox",
8218
+ defaultValue: false,
8219
+ label: "Populaire",
8220
+ admin: { position: "sidebar" }
8221
+ },
8018
8222
  {
8019
8223
  name: "sortOrder",
8020
8224
  type: "number",
8021
8225
  defaultValue: 0,
8022
8226
  label: "Ordre",
8023
8227
  admin: { position: "sidebar" }
8228
+ },
8229
+ {
8230
+ name: "helpfulYes",
8231
+ type: "number",
8232
+ defaultValue: 0,
8233
+ label: "Votes utiles",
8234
+ admin: { position: "sidebar", readOnly: true }
8235
+ },
8236
+ {
8237
+ name: "helpfulNo",
8238
+ type: "number",
8239
+ defaultValue: 0,
8240
+ label: "Votes non utiles",
8241
+ admin: { position: "sidebar", readOnly: true }
8024
8242
  }
8025
8243
  ],
8026
8244
  hooks: {