443 lines
16 KiB
Dart
443 lines
16 KiB
Dart
class EmailTemplateData {
|
|
final String templateName;
|
|
final String subject;
|
|
final String htmlBody;
|
|
final int? templateId;
|
|
final List<EmailTemplatePlaceholder> placeholders;
|
|
|
|
const EmailTemplateData({
|
|
this.templateName = 'Enrollment Reminder Mail',
|
|
required this.subject,
|
|
required this.htmlBody,
|
|
this.templateId,
|
|
this.placeholders = const [],
|
|
});
|
|
|
|
static EmailTemplateData fromApiResponse(
|
|
Map<String, dynamic> response, {
|
|
required String defaultSubject,
|
|
required String defaultHtmlBody,
|
|
String defaultTemplateName = 'Enrollment Reminder Mail',
|
|
}) {
|
|
final data = response['data'];
|
|
Map<String, dynamic> source;
|
|
Map<String, dynamic>? parentData;
|
|
if (data is Map) {
|
|
parentData = Map<String, dynamic>.from(data);
|
|
final nested = parentData['email_template'] ?? parentData['template'];
|
|
source = nested is Map
|
|
? Map<String, dynamic>.from(nested)
|
|
: parentData;
|
|
} else {
|
|
source = Map<String, dynamic>.from(response);
|
|
}
|
|
|
|
final templateName = _readString(source, const [
|
|
'template_name',
|
|
'name',
|
|
'mail_template_name',
|
|
]);
|
|
final subject = _readString(source, const [
|
|
'email_subject',
|
|
'subject',
|
|
'mail_subject',
|
|
]);
|
|
final htmlBody = _readString(source, const [
|
|
'email_body',
|
|
'email_html',
|
|
'html_body',
|
|
'body',
|
|
'template_html',
|
|
'mail_body',
|
|
]);
|
|
|
|
final idRaw = source['id'] ?? source['template_id'];
|
|
final templateId = idRaw == null ? null : int.tryParse(idRaw.toString());
|
|
|
|
final placeholders = parsePlaceholders(
|
|
source['place_holders'] ??
|
|
source['placeholders'] ??
|
|
parentData?['place_holders'] ??
|
|
parentData?['placeholders'],
|
|
);
|
|
|
|
return EmailTemplateData(
|
|
templateName:
|
|
templateName.isNotEmpty ? templateName : defaultTemplateName,
|
|
subject: subject.isNotEmpty ? subject : defaultSubject,
|
|
htmlBody: htmlBody.isNotEmpty ? htmlBody : defaultHtmlBody,
|
|
templateId: templateId,
|
|
placeholders:
|
|
placeholders.isNotEmpty ? placeholders : defaultPlaceholders(),
|
|
);
|
|
}
|
|
|
|
static List<EmailTemplatePlaceholder> defaultPlaceholders() {
|
|
return parsePlaceholders(kDefaultEnrollmentReminderPlaceholderKeys);
|
|
}
|
|
|
|
static List<EmailTemplatePlaceholder> parsePlaceholders(dynamic raw) {
|
|
if (raw is! List || raw.isEmpty) {
|
|
return const [];
|
|
}
|
|
|
|
final placeholders = <EmailTemplatePlaceholder>[];
|
|
for (final item in raw) {
|
|
final key = item?.toString().trim();
|
|
if (key == null || key.isEmpty) continue;
|
|
placeholders.add(
|
|
EmailTemplatePlaceholder(
|
|
formatPlaceholderLabel(key),
|
|
'{{$key}}',
|
|
),
|
|
);
|
|
}
|
|
|
|
return placeholders;
|
|
}
|
|
|
|
static String formatPlaceholderLabel(String key) {
|
|
return key
|
|
.split('_')
|
|
.where((part) => part.isNotEmpty)
|
|
.map(
|
|
(part) =>
|
|
'${part[0].toUpperCase()}${part.substring(1).toLowerCase()}',
|
|
)
|
|
.join(' ');
|
|
}
|
|
|
|
static String _readString(Map<String, dynamic> map, List<String> keys) {
|
|
for (final key in keys) {
|
|
final value = map[key];
|
|
if (value != null && value.toString().trim().isNotEmpty) {
|
|
return value.toString();
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
static String wrapPlainTextAsHtml(String text) {
|
|
final escaped = text
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('\n', '<br/>');
|
|
return '''<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
</head>
|
|
<body style="font-family: Arial, Helvetica, sans-serif; line-height: 1.6; color: #333333; background-color: #ffffff; max-width: 500px; margin: 0 auto; padding: 24px; text-align: left;">
|
|
<p>$escaped</p>
|
|
</body>
|
|
</html>''';
|
|
}
|
|
}
|
|
|
|
class EmailTemplatePlaceholder {
|
|
final String label;
|
|
final String token;
|
|
|
|
const EmailTemplatePlaceholder(this.label, this.token);
|
|
}
|
|
|
|
const kDefaultEnrollmentReminderPlaceholderKeys = [
|
|
'member_name',
|
|
'nhance_logo',
|
|
'client_logo',
|
|
'policy_name',
|
|
'policy_no',
|
|
'emp_code',
|
|
'app_link',
|
|
'client_name',
|
|
'enrollment_open_date',
|
|
'enrollment_close_date',
|
|
];
|
|
|
|
const kDefaultEnrollmentReminderHtmlBody = r'''<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Enrollment Reminder</title>
|
|
</head>
|
|
<body style="margin:0;padding:0;background-color:#f4f6f8;font-family:Arial,Helvetica,sans-serif;">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f6f8;padding:24px 0;">
|
|
<tr>
|
|
<td align="center">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background-color:#ffffff;border-radius:10px;overflow:hidden;">
|
|
<tr>
|
|
<td align="center" style="background-color:#009195;padding:24px;">
|
|
<img src="{{client_logo}}" alt="Company Logo" width="160" style="display:block;margin:0 auto 12px;max-height:48px;width:auto;border:0;" />
|
|
<h1 style="margin:0;font-size:22px;line-height:28px;color:#ffffff;font-weight:600;">
|
|
Enrollment Reminder
|
|
</h1>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:32px 28px;color:#1f2937;font-size:15px;line-height:24px;">
|
|
<p style="margin:0 0 16px;">
|
|
Dear <strong>{{member_name}}</strong>,
|
|
</p>
|
|
<p style="margin:0 0 20px;">
|
|
This is a reminder to complete your insurance enrollment for the policy below. Please finish enrollment before the deadline to avoid missing your benefits.
|
|
</p>
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;background-color:#f9fafb;margin-bottom:24px;">
|
|
<tr>
|
|
<td style="padding:16px 18px;font-size:14px;line-height:22px;color:#374151;">
|
|
<strong style="color:#111827;">Policy Number:</strong> {{policy_no}}<br>
|
|
<strong style="color:#111827;">Enrollment Open Date:</strong> {{enrollment_open_date}}<br>
|
|
<strong style="color:#111827;">Enrollment Close Date:</strong> {{enrollment_close_date}}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 auto 24px;">
|
|
<tr>
|
|
<td align="center" style="border-radius:6px;background-color:#009195;">
|
|
<a href="{{app_link}}" target="_blank" style="display:inline-block;padding:14px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:6px;">
|
|
Complete Enrollment
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p style="margin:0 0 8px;">
|
|
If you have already completed your enrollment, please ignore this email.
|
|
</p>
|
|
<p style="margin:0;">
|
|
Regards,<br>
|
|
<strong>{{client_name}}</strong>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td align="center" style="padding:18px 28px;background-color:#f9fafb;font-size:12px;line-height:18px;color:#6b7280;">
|
|
This is an automated reminder email. Please do not reply directly.<br>
|
|
<img src="{{nhance_logo}}" alt="Nhance" width="80" style="display:block;margin:12px auto 0;max-height:24px;width:auto;border:0;" />
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>''';
|
|
|
|
const kEmailContentBlocks = <({String label, String snippet})>[
|
|
(
|
|
label: 'Columns',
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0"><tr><td width="50%" style="padding:8px;vertical-align:top;">Column 1</td><td width="50%" style="padding:8px;vertical-align:top;">Column 2</td></tr></table>',
|
|
),
|
|
(
|
|
label: 'Button',
|
|
snippet:
|
|
'<p style="text-align:center;margin:16px 0;"><a href="{{app_link}}" style="background:#009195;color:#fff;padding:12px 24px;text-decoration:none;border-radius:4px;display:inline-block;font-weight:600;">Click Here</a></p>',
|
|
),
|
|
(
|
|
label: 'Divider',
|
|
snippet:
|
|
'<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0;" />',
|
|
),
|
|
(
|
|
label: 'Heading',
|
|
snippet:
|
|
'<h2 style="color:#009195;font-size:20px;margin:16px 0 8px;font-weight:600;">Heading</h2>',
|
|
),
|
|
(
|
|
label: 'Paragraph',
|
|
snippet:
|
|
'<p style="margin:8px 0;line-height:1.6;font-size:14px;">Paragraph text</p>',
|
|
),
|
|
(
|
|
label: 'Image',
|
|
snippet:
|
|
'<p style="text-align:center;margin:12px 0;"><img src="{{client_logo}}" alt="Logo" style="max-width:200px;height:auto;" /></p>',
|
|
),
|
|
(
|
|
label: 'Social',
|
|
snippet:
|
|
'<p style="text-align:center;margin:12px 0;"><a href="#" style="margin:0 8px;color:#009195;">Facebook</a><a href="#" style="margin:0 8px;color:#009195;">LinkedIn</a></p>',
|
|
),
|
|
(
|
|
label: 'Menu',
|
|
snippet:
|
|
'<p style="text-align:center;margin:12px 0;font-size:13px;"><a href="#" style="margin:0 8px;color:#009195;">Home</a>|<a href="#" style="margin:0 8px;color:#009195;">Support</a></p>',
|
|
),
|
|
(
|
|
label: 'HTML',
|
|
snippet: '<!-- custom html -->',
|
|
),
|
|
];
|
|
|
|
/// Column layout blocks (Unlayer Blocks > Blank tab).
|
|
class EmailColumnLayout {
|
|
final List<int> ratios;
|
|
final String snippet;
|
|
|
|
const EmailColumnLayout({
|
|
required this.ratios,
|
|
required this.snippet,
|
|
});
|
|
}
|
|
|
|
const kEmailColumnLayouts = <EmailColumnLayout>[
|
|
EmailColumnLayout(
|
|
ratios: [100],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td style="padding:8px;vertical-align:top;border:1px dashed #ccc;min-height:40px;"> </td></tr></table>',
|
|
),
|
|
EmailColumnLayout(
|
|
ratios: [50, 50],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td width="50%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="50%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td></tr></table>',
|
|
),
|
|
EmailColumnLayout(
|
|
ratios: [33, 33, 33],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td width="33%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="33%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="34%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td></tr></table>',
|
|
),
|
|
EmailColumnLayout(
|
|
ratios: [25, 75],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="75%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td></tr></table>',
|
|
),
|
|
EmailColumnLayout(
|
|
ratios: [75, 25],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td width="75%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td></tr></table>',
|
|
),
|
|
EmailColumnLayout(
|
|
ratios: [25, 25, 25, 25],
|
|
snippet:
|
|
'<table width="100%" cellpadding="0" cellspacing="0" style="margin:8px 0;"><tr><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;"> </td></tr></table>',
|
|
),
|
|
];
|
|
|
|
class EmailBodyStyle {
|
|
final String backgroundColor;
|
|
final String contentWidth;
|
|
final String fontFamily;
|
|
final String textColor;
|
|
final String linkColor;
|
|
final String contentAlignment;
|
|
final String fontWeight;
|
|
|
|
const EmailBodyStyle({
|
|
this.backgroundColor = '#ffffff',
|
|
this.contentWidth = '500px',
|
|
this.fontFamily = 'Arial, Helvetica, sans-serif',
|
|
this.textColor = '#000000',
|
|
this.linkColor = '#009195',
|
|
this.contentAlignment = 'left',
|
|
this.fontWeight = '400',
|
|
});
|
|
|
|
int get contentWidthPx {
|
|
final n = int.tryParse(contentWidth.replaceAll('px', '').trim());
|
|
return n ?? 500;
|
|
}
|
|
|
|
EmailBodyStyle copyWith({
|
|
String? backgroundColor,
|
|
String? contentWidth,
|
|
String? fontFamily,
|
|
String? textColor,
|
|
String? linkColor,
|
|
String? contentAlignment,
|
|
String? fontWeight,
|
|
}) {
|
|
return EmailBodyStyle(
|
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
|
contentWidth: contentWidth ?? this.contentWidth,
|
|
fontFamily: fontFamily ?? this.fontFamily,
|
|
textColor: textColor ?? this.textColor,
|
|
linkColor: linkColor ?? this.linkColor,
|
|
contentAlignment: contentAlignment ?? this.contentAlignment,
|
|
fontWeight: fontWeight ?? this.fontWeight,
|
|
);
|
|
}
|
|
|
|
static EmailBodyStyle parseFromHtml(String html) {
|
|
final bgMatch = RegExp(
|
|
r'background(?:-color)?:\s*([^;"]+)',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
final widthMatch = RegExp(
|
|
r'max-width:\s*(\d+)px',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
final fontMatch = RegExp(
|
|
r'font-family:\s*([^;"]+)',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
final colorMatch = RegExp(
|
|
r'(?<!background-)color:\s*([^;"]+)',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
final alignMatch = RegExp(
|
|
r'text-align:\s*([^;"]+)',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
final weightMatch = RegExp(
|
|
r'font-weight:\s*([^;"]+)',
|
|
caseSensitive: false,
|
|
).firstMatch(html);
|
|
|
|
return EmailBodyStyle(
|
|
backgroundColor: bgMatch?.group(1)?.trim() ?? '#ffffff',
|
|
contentWidth: '${widthMatch?.group(1)?.trim() ?? '500'}px',
|
|
fontFamily: fontMatch?.group(1)?.trim() ?? 'Arial, Helvetica, sans-serif',
|
|
textColor: colorMatch?.group(1)?.trim() ?? '#000000',
|
|
contentAlignment: alignMatch?.group(1)?.trim() ?? 'left',
|
|
fontWeight: weightMatch?.group(1)?.trim() ?? '400',
|
|
);
|
|
}
|
|
}
|
|
|
|
String applyEmailBodyStyles(String html, EmailBodyStyle style) {
|
|
final bodyStyle =
|
|
'font-family: ${style.fontFamily}; line-height: 1.6; color: ${style.textColor}; '
|
|
'font-weight: ${style.fontWeight}; background-color: ${style.backgroundColor}; '
|
|
'max-width: ${style.contentWidth}; margin: 0 auto; padding: 24px; '
|
|
'text-align: ${style.contentAlignment};';
|
|
|
|
final lower = html.toLowerCase();
|
|
final bodyStart = lower.indexOf('<body');
|
|
if (bodyStart == -1) {
|
|
return EmailTemplateData.wrapPlainTextAsHtml(html);
|
|
}
|
|
|
|
final openEnd = html.indexOf('>', bodyStart);
|
|
if (openEnd == -1) return html;
|
|
|
|
final bodyTag = html.substring(bodyStart, openEnd + 1);
|
|
final hasStyle = bodyTag.toLowerCase().contains('style=');
|
|
|
|
String newBodyTag;
|
|
if (hasStyle) {
|
|
newBodyTag = bodyTag.replaceAll(
|
|
RegExp(r'style="[^"]*"', caseSensitive: false),
|
|
'style="$bodyStyle"',
|
|
);
|
|
} else {
|
|
newBodyTag = bodyTag.replaceFirst('>', ' style="$bodyStyle">');
|
|
}
|
|
|
|
var result = html.replaceRange(bodyStart, openEnd + 1, newBodyTag);
|
|
|
|
final linkStyle = '<style>a{color:${style.linkColor};}</style>';
|
|
if (!result.toLowerCase().contains('<style>')) {
|
|
final headEnd = result.toLowerCase().indexOf('</head>');
|
|
if (headEnd != -1) {
|
|
result =
|
|
'${result.substring(0, headEnd)}$linkStyle${result.substring(headEnd)}';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
enum ReminderEmailTemplateMode { config, send }
|