enrollment-app/lib/presentation/email_template/email_template_models.dart
2026-07-02 15:39:19 +05:30

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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.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;">&nbsp;</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;">&nbsp;</td><td width="50%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</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;">&nbsp;</td><td width="33%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</td><td width="34%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</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;">&nbsp;</td><td width="75%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</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;">&nbsp;</td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</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;">&nbsp;</td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</td><td width="25%" style="padding:8px;vertical-align:top;border:1px dashed #ccc;">&nbsp;</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 }