@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.cjs +303 -85
- package/dist/index.js +303 -85
- package/dist/styles/TicketDetail.module.scss +3 -1
- package/package.json +1 -1
- package/src/collections/KnowledgeBase.ts +29 -0
- package/src/collections/SupportClients.ts +115 -0
- package/src/collections/TicketMessages.ts +21 -3
- package/src/collections/Tickets.ts +22 -3
- package/src/collections/TimeEntries.ts +12 -0
- package/src/hooks/ticketStatusEmail.ts +38 -27
- package/src/styles/TicketDetail.module.scss +3 -1
- package/src/utils/emailTemplate.ts +153 -61
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:
|
|
1525
|
-
secondaryColor:
|
|
1526
|
-
accentColor:
|
|
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:
|
|
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: ${
|
|
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",
|
|
1577
|
-
result = styleTag(result, "p",
|
|
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:
|
|
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(
|
|
1595
|
+
function getButtonColors(_config) {
|
|
1582
1596
|
return {
|
|
1583
|
-
primary: { bg:
|
|
1584
|
-
secondary: { bg:
|
|
1585
|
-
dark: { bg:
|
|
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
|
-
|
|
1590
|
-
const colors = getButtonColors(
|
|
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:
|
|
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
|
-
|
|
1610
|
-
const color = borderColor ||
|
|
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:
|
|
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:
|
|
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="
|
|
1644
|
+
<div style="${panel}">
|
|
1630
1645
|
${parts.join("")}
|
|
1631
1646
|
</div>
|
|
1632
1647
|
`;
|
|
1633
1648
|
}
|
|
1634
1649
|
return `
|
|
1635
|
-
<div style="
|
|
1636
|
-
<p style="margin: 0; font-size: 15px; line-height: 1.75; color:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: ${
|
|
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
|
|
1663
|
-
|
|
1664
|
-
|
|
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(" · ")}</p>` : "";
|
|
1665
1697
|
return `
|
|
1666
1698
|
<!-- Spacer -->
|
|
1667
|
-
<div style="height:
|
|
1699
|
+
<div style="height: 28px;"></div>
|
|
1668
1700
|
|
|
1669
|
-
<!--
|
|
1670
|
-
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom:
|
|
1701
|
+
<!-- Hairline separator -->
|
|
1702
|
+
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 22px;">
|
|
1671
1703
|
<tr>
|
|
1672
|
-
<td
|
|
1673
|
-
<td width="25%" height="3" bgcolor="${config.secondaryColor}" style="font-size:1px;line-height:1px;"> </td>
|
|
1674
|
-
<td width="25%" height="3" bgcolor="${config.accentColor}" style="font-size:1px;line-height:1px;"> </td>
|
|
1704
|
+
<td height="1" bgcolor="${COLORS.line}" style="font-size:1px;line-height:1px;"> </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:
|
|
1686
|
-
${contactParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 13px; color:
|
|
1687
|
-
${locationParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 12px; color:
|
|
1688
|
-
${
|
|
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(" · ")}</p>` : ""}
|
|
1717
|
+
${locationParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 12px; color: ${COLORS.faint}; line-height: 1.4;">${locationParts.join(" · ")}</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 {
|
|
1697
|
-
const
|
|
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:
|
|
1700
|
-
<span style="color: #fff; font-weight:
|
|
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:
|
|
1716
|
-
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">${escapeHtml(preheader)}
|
|
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)} ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌</div>` : ""}
|
|
1717
1749
|
|
|
1718
1750
|
<!-- Outer wrapper -->
|
|
1719
|
-
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:
|
|
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
|
|
1723
|
-
<table cellpadding="0" cellspacing="0" border="0" width="
|
|
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;"> </td>
|
|
1760
|
+
</tr>
|
|
1724
1761
|
|
|
1725
1762
|
<!-- Header -->
|
|
1726
1763
|
<tr>
|
|
1727
|
-
<td style="background: ${
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
5976
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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° 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:
|
|
5980
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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:
|
|
5984
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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
|
|
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:
|
|
6024
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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° 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:
|
|
6028
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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:
|
|
6032
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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:
|
|
6036
|
-
<td style="padding: 8px 0; font-size: 15px; color: #
|
|
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
|
|
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:
|
|
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 ? "#
|
|
7354
|
+
emailQuote(preview, isNewTicket ? "#1d2b4d" : "#17807c"),
|
|
7293
7355
|
emailButton("Ouvrir dans l'admin", adminUrl, "dark")
|
|
7294
|
-
].join(""), {
|
|
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: {
|