@huloglobal/vendure-plugin-email-tracking 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +34 -0
- package/README.md +129 -0
- package/dist/email-log.entity.d.ts +57 -0
- package/dist/email-log.entity.d.ts.map +1 -0
- package/dist/email-log.entity.js +147 -0
- package/dist/email-log.entity.js.map +1 -0
- package/dist/email-tracking.controller.d.ts +51 -0
- package/dist/email-tracking.controller.d.ts.map +1 -0
- package/dist/email-tracking.controller.js +260 -0
- package/dist/email-tracking.controller.js.map +1 -0
- package/dist/email-tracking.service.d.ts +48 -0
- package/dist/email-tracking.service.d.ts.map +1 -0
- package/dist/email-tracking.service.js +196 -0
- package/dist/email-tracking.service.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +43 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +52 -0
- package/dist/options.js.map +1 -0
- package/dist/plugin.d.ts +53 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +118 -0
- package/dist/plugin.js.map +1 -0
- package/dist/proxy-headers.d.ts +37 -0
- package/dist/proxy-headers.d.ts.map +1 -0
- package/dist/proxy-headers.js +81 -0
- package/dist/proxy-headers.js.map +1 -0
- package/dist/tracking-email-sender.d.ts +22 -0
- package/dist/tracking-email-sender.d.ts.map +1 -0
- package/dist/tracking-email-sender.js +117 -0
- package/dist/tracking-email-sender.js.map +1 -0
- package/package.json +53 -0
- package/ui/components/email-log.component.ts +372 -0
- package/ui/email-log-nav.module.ts +24 -0
- package/ui/email-log.module.ts +17 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
|
2
|
+
import { ActivatedRoute } from '@angular/router';
|
|
3
|
+
import { HttpClient } from '@angular/common/http';
|
|
4
|
+
import { NotificationService } from '@vendure/admin-ui/core';
|
|
5
|
+
|
|
6
|
+
interface EmailRow {
|
|
7
|
+
id: number;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
type: string;
|
|
10
|
+
recipient: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
status: string;
|
|
13
|
+
customerId: number | null;
|
|
14
|
+
orderId: number | null;
|
|
15
|
+
orderCode: string | null;
|
|
16
|
+
invoiceId: number | null;
|
|
17
|
+
applicationId: number | null;
|
|
18
|
+
channelId: number;
|
|
19
|
+
openCount: number;
|
|
20
|
+
firstOpenedAt: string | null;
|
|
21
|
+
lastOpenedAt: string | null;
|
|
22
|
+
clickCount: number;
|
|
23
|
+
firstClickedAt: string | null;
|
|
24
|
+
smtpResponse: string | null;
|
|
25
|
+
errorMessage: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EmailDetail extends EmailRow {
|
|
29
|
+
fromAddress: string | null;
|
|
30
|
+
bcc: string | null;
|
|
31
|
+
replyTo: string | null;
|
|
32
|
+
context: string | null;
|
|
33
|
+
smtpMessageId: string | null;
|
|
34
|
+
firstOpenIp: string | null;
|
|
35
|
+
firstOpenUserAgent: string | null;
|
|
36
|
+
clicks: Array<{ url: string; ts: string; ip: string | null; ua: string | null }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Component({
|
|
40
|
+
selector: 'ees-email-log',
|
|
41
|
+
standalone: false,
|
|
42
|
+
template: `
|
|
43
|
+
<vdr-page-block>
|
|
44
|
+
<vdr-action-bar>
|
|
45
|
+
<vdr-ab-left><h2>Email Log</h2></vdr-ab-left>
|
|
46
|
+
<vdr-ab-right>
|
|
47
|
+
<button class="btn btn-link" (click)="load()" [disabled]="loading">
|
|
48
|
+
<clr-icon shape="refresh"></clr-icon> Refresh
|
|
49
|
+
</button>
|
|
50
|
+
</vdr-ab-right>
|
|
51
|
+
</vdr-action-bar>
|
|
52
|
+
</vdr-page-block>
|
|
53
|
+
|
|
54
|
+
<vdr-page-block>
|
|
55
|
+
<div class="summary-row">
|
|
56
|
+
<div class="summary-card" [class.active]="filterStatus===''" (click)="setStatus('')">
|
|
57
|
+
<div class="num" style="color:#1d4ed8">{{ totalAll() }}</div>
|
|
58
|
+
<div class="lbl">Last {{ summary.fromDays }} days</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="summary-card" [class.active]="filterStatus==='sent'" (click)="setStatus('sent')">
|
|
61
|
+
<div class="num" style="color:#10b981">{{ summary.sent || 0 }}</div>
|
|
62
|
+
<div class="lbl">Sent</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="summary-card" [class.active]="filterStatus==='failed'" (click)="setStatus('failed')">
|
|
65
|
+
<div class="num" style="color:#ef4444">{{ summary.failed || 0 }}</div>
|
|
66
|
+
<div class="lbl">Failed</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="summary-card" [class.active]="filterStatus==='deferred'" (click)="setStatus('deferred')">
|
|
69
|
+
<div class="num" style="color:#f59e0b">{{ summary.deferred || 0 }}</div>
|
|
70
|
+
<div class="lbl">Deferred</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="summary-card" [class.active]="filterStatus==='bounced'" (click)="setStatus('bounced')">
|
|
73
|
+
<div class="num" style="color:#9333ea">{{ summary.bounced || 0 }}</div>
|
|
74
|
+
<div class="lbl">Bounced</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="summary-card">
|
|
77
|
+
<div class="num" style="color:#0369a1">{{ summary.opens || 0 }}</div>
|
|
78
|
+
<div class="lbl">Opens</div>
|
|
79
|
+
<div class="sub">{{ summary.clicks || 0 }} clicks</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</vdr-page-block>
|
|
83
|
+
|
|
84
|
+
<vdr-page-block>
|
|
85
|
+
<div class="card">
|
|
86
|
+
<div class="card-block">
|
|
87
|
+
<div class="filters">
|
|
88
|
+
<input class="form-input" placeholder="Filter recipient…" [(ngModel)]="filterRecipient" (keyup.enter)="load()">
|
|
89
|
+
<input class="form-input" placeholder="Filter type…" [(ngModel)]="filterType" (keyup.enter)="load()">
|
|
90
|
+
<input class="form-input" placeholder="Customer ID" [(ngModel)]="filterCustomerId" (keyup.enter)="load()">
|
|
91
|
+
<input class="form-input" placeholder="Order code" [(ngModel)]="filterOrderCode" (keyup.enter)="load()">
|
|
92
|
+
<button class="btn btn-secondary" (click)="load()">Apply</button>
|
|
93
|
+
<button class="btn btn-link" (click)="clearFilters()">Clear</button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div *ngIf="loading" style="padding:30px;text-align:center;color:var(--color-component-color-300)">Loading…</div>
|
|
97
|
+
<div *ngIf="!loading && rows.length === 0" style="padding:30px;text-align:center;color:var(--color-component-color-300)">
|
|
98
|
+
No emails match this view.
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<table class="table table-compact" *ngIf="rows.length > 0">
|
|
102
|
+
<thead>
|
|
103
|
+
<tr>
|
|
104
|
+
<th>#</th>
|
|
105
|
+
<th>Sent</th>
|
|
106
|
+
<th>Type</th>
|
|
107
|
+
<th>Recipient</th>
|
|
108
|
+
<th>Subject</th>
|
|
109
|
+
<th>Status</th>
|
|
110
|
+
<th>Opens</th>
|
|
111
|
+
<th>Clicks</th>
|
|
112
|
+
<th>Linked to</th>
|
|
113
|
+
<th></th>
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
<ng-container *ngFor="let r of rows">
|
|
118
|
+
<tr>
|
|
119
|
+
<td><strong>#{{ r.id }}</strong></td>
|
|
120
|
+
<td>{{ r.createdAt | date:'short' }}</td>
|
|
121
|
+
<td><span class="pill type-pill">{{ r.type }}</span></td>
|
|
122
|
+
<td>
|
|
123
|
+
<a [href]="'mailto:' + r.recipient">{{ r.recipient }}</a>
|
|
124
|
+
</td>
|
|
125
|
+
<td style="max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" [title]="r.subject">{{ r.subject }}</td>
|
|
126
|
+
<td>
|
|
127
|
+
<span class="pill" [ngClass]="'status-' + r.status">{{ r.status }}</span>
|
|
128
|
+
</td>
|
|
129
|
+
<td>
|
|
130
|
+
<span [class.muted]="r.openCount === 0" [style.color]="r.openCount > 0 ? '#10b981' : null">{{ r.openCount }}</span>
|
|
131
|
+
<div class="help-text" *ngIf="r.firstOpenedAt">{{ r.firstOpenedAt | date:'short' }}</div>
|
|
132
|
+
</td>
|
|
133
|
+
<td>
|
|
134
|
+
<span [class.muted]="r.clickCount === 0" [style.color]="r.clickCount > 0 ? '#0369a1' : null">{{ r.clickCount }}</span>
|
|
135
|
+
</td>
|
|
136
|
+
<td>
|
|
137
|
+
<a *ngIf="r.orderId" [routerLink]="['/orders', r.orderId]">{{ r.orderCode || r.orderId }}</a>
|
|
138
|
+
<a *ngIf="r.customerId && !r.orderId" [routerLink]="['/customers', r.customerId]">cust #{{ r.customerId }}</a>
|
|
139
|
+
<span *ngIf="!r.orderId && !r.customerId" class="help-text">—</span>
|
|
140
|
+
</td>
|
|
141
|
+
<td>
|
|
142
|
+
<button class="btn btn-sm btn-link" (click)="open(r.id)">
|
|
143
|
+
<clr-icon shape="eye"></clr-icon> Details
|
|
144
|
+
</button>
|
|
145
|
+
</td>
|
|
146
|
+
</tr>
|
|
147
|
+
<tr *ngIf="expandedId === r.id && detail" class="detail-row">
|
|
148
|
+
<td colspan="10">
|
|
149
|
+
<div class="detail-grid">
|
|
150
|
+
<div>
|
|
151
|
+
<div class="lbl">From</div><div>{{ detail.fromAddress }}</div>
|
|
152
|
+
<div class="lbl">To</div><div>{{ detail.recipient }}</div>
|
|
153
|
+
<div class="lbl" *ngIf="detail.bcc">BCC</div><div *ngIf="detail.bcc">{{ detail.bcc }}</div>
|
|
154
|
+
<div class="lbl">Subject</div><div>{{ detail.subject }}</div>
|
|
155
|
+
<div class="lbl">Context</div><div>{{ detail.context || '—' }}</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<div class="lbl">SMTP message id</div>
|
|
159
|
+
<div style="font-family:monospace;font-size:11px;word-break:break-all">{{ detail.smtpMessageId || '—' }}</div>
|
|
160
|
+
<div class="lbl">SMTP response</div>
|
|
161
|
+
<div style="font-family:monospace;font-size:11px;word-break:break-all">{{ detail.smtpResponse || '—' }}</div>
|
|
162
|
+
<div class="lbl" *ngIf="detail.errorMessage">Error</div>
|
|
163
|
+
<div *ngIf="detail.errorMessage" style="color:#ef4444">{{ detail.errorMessage }}</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div>
|
|
166
|
+
<div class="lbl">Opens</div>
|
|
167
|
+
<div>{{ detail.openCount }} ({{ detail.firstOpenedAt | date:'short' }} → {{ detail.lastOpenedAt | date:'short' }})</div>
|
|
168
|
+
<div class="lbl" *ngIf="detail.firstOpenIp">First open IP</div>
|
|
169
|
+
<div *ngIf="detail.firstOpenIp" style="font-family:monospace;font-size:11px">{{ detail.firstOpenIp }}</div>
|
|
170
|
+
<div class="lbl" *ngIf="detail.firstOpenUserAgent">First open UA</div>
|
|
171
|
+
<div *ngIf="detail.firstOpenUserAgent" style="font-size:11px;color:var(--color-component-color-300)">{{ detail.firstOpenUserAgent }}</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<h5 style="margin-top:15px" *ngIf="detail.clicks.length > 0">Clicks ({{ detail.clickCount }})</h5>
|
|
175
|
+
<table class="table table-compact" *ngIf="detail.clicks.length > 0">
|
|
176
|
+
<thead><tr><th>Time</th><th>URL</th><th>IP</th><th>UA</th></tr></thead>
|
|
177
|
+
<tbody>
|
|
178
|
+
<tr *ngFor="let c of detail.clicks">
|
|
179
|
+
<td>{{ c.ts | date:'short' }}</td>
|
|
180
|
+
<td style="font-family:monospace;font-size:11px;word-break:break-all"><a [href]="c.url" target="_blank">{{ c.url }}</a></td>
|
|
181
|
+
<td>{{ c.ip }}</td>
|
|
182
|
+
<td style="font-size:11px;color:var(--color-component-color-300)">{{ c.ua }}</td>
|
|
183
|
+
</tr>
|
|
184
|
+
</tbody>
|
|
185
|
+
</table>
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
</ng-container>
|
|
189
|
+
</tbody>
|
|
190
|
+
</table>
|
|
191
|
+
|
|
192
|
+
<div class="pager" *ngIf="total > take">
|
|
193
|
+
<button class="btn btn-sm" (click)="prevPage()" [disabled]="skip === 0">‹ Prev</button>
|
|
194
|
+
<span style="margin:0 12px;font-size:12px">Showing {{ skip + 1 }}–{{ skip + rows.length }} of {{ total }}</span>
|
|
195
|
+
<button class="btn btn-sm" (click)="nextPage()" [disabled]="skip + take >= total">Next ›</button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</vdr-page-block>
|
|
200
|
+
`,
|
|
201
|
+
styles: [`
|
|
202
|
+
:host { color: var(--color-text-100, inherit); }
|
|
203
|
+
.summary-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
204
|
+
.summary-card {
|
|
205
|
+
flex: 1; min-width: 130px; padding: 14px 18px;
|
|
206
|
+
border: 1px solid var(--color-component-border-200);
|
|
207
|
+
border-radius: 6px;
|
|
208
|
+
background: var(--color-component-bg-100);
|
|
209
|
+
color: var(--color-text-100, inherit);
|
|
210
|
+
cursor: pointer; transition: border-color .15s, box-shadow .15s;
|
|
211
|
+
}
|
|
212
|
+
.summary-card:hover { border-color: var(--color-primary-500, #1d4ed8); }
|
|
213
|
+
.summary-card.active {
|
|
214
|
+
border-color: var(--color-primary-500, #1d4ed8);
|
|
215
|
+
box-shadow: 0 0 0 2px rgba(29,78,216,.18);
|
|
216
|
+
}
|
|
217
|
+
.summary-card .num { font-size: 24px; font-weight: 700; line-height: 1.2; }
|
|
218
|
+
.summary-card .lbl { font-size: 11px; color: var(--color-component-color-300); margin-top: 2px; }
|
|
219
|
+
.summary-card .sub { font-size: 10px; color: var(--color-component-color-300); margin-top: 2px; }
|
|
220
|
+
|
|
221
|
+
.filters { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
222
|
+
.filters .form-input {
|
|
223
|
+
padding: 4px 8px; height: 32px;
|
|
224
|
+
border: 1px solid var(--color-component-border-200);
|
|
225
|
+
background: var(--color-component-bg-100);
|
|
226
|
+
color: var(--color-text-100, inherit);
|
|
227
|
+
border-radius: 4px; min-width: 160px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.pill {
|
|
231
|
+
display: inline-block; padding: 2px 8px; border-radius: 10px;
|
|
232
|
+
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
233
|
+
color: #fff;
|
|
234
|
+
}
|
|
235
|
+
.type-pill {
|
|
236
|
+
background: var(--color-component-bg-200);
|
|
237
|
+
color: var(--color-text-100, inherit);
|
|
238
|
+
border: 1px solid var(--color-component-border-200);
|
|
239
|
+
}
|
|
240
|
+
.status-sent { background: #10b981; }
|
|
241
|
+
.status-failed { background: #ef4444; }
|
|
242
|
+
.status-deferred { background: #f59e0b; }
|
|
243
|
+
.status-bounced { background: #9333ea; }
|
|
244
|
+
.status-complained { background: #db2777; }
|
|
245
|
+
|
|
246
|
+
.detail-row > td {
|
|
247
|
+
background: var(--color-component-bg-200);
|
|
248
|
+
color: var(--color-text-100, inherit);
|
|
249
|
+
padding: 18px;
|
|
250
|
+
border-top: 1px solid var(--color-component-border-200);
|
|
251
|
+
}
|
|
252
|
+
.detail-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
|
|
253
|
+
.detail-grid .lbl {
|
|
254
|
+
font-size: 11px;
|
|
255
|
+
color: var(--color-component-color-300);
|
|
256
|
+
text-transform: uppercase; margin-top: 6px;
|
|
257
|
+
}
|
|
258
|
+
.pager {
|
|
259
|
+
display: flex; align-items: center; justify-content: flex-end;
|
|
260
|
+
padding: 10px 0;
|
|
261
|
+
color: var(--color-text-100, inherit);
|
|
262
|
+
}
|
|
263
|
+
.help-text { color: var(--color-component-color-300); font-size: 11px; }
|
|
264
|
+
.muted { color: var(--color-component-color-300); }
|
|
265
|
+
`],
|
|
266
|
+
})
|
|
267
|
+
export class EmailLogComponent implements OnInit {
|
|
268
|
+
rows: EmailRow[] = [];
|
|
269
|
+
total = 0;
|
|
270
|
+
take = 100;
|
|
271
|
+
skip = 0;
|
|
272
|
+
loading = false;
|
|
273
|
+
|
|
274
|
+
filterStatus = '';
|
|
275
|
+
filterRecipient = '';
|
|
276
|
+
filterType = '';
|
|
277
|
+
filterCustomerId = '';
|
|
278
|
+
filterOrderCode = '';
|
|
279
|
+
/** When the page is mounted with a ?customerId= query param, lock to that
|
|
280
|
+
* customer (used by the per-customer Emails tab). */
|
|
281
|
+
lockedCustomerId: string | null = null;
|
|
282
|
+
|
|
283
|
+
summary: any = { sent: 0, failed: 0, deferred: 0, bounced: 0, opens: 0, clicks: 0, fromDays: 30 };
|
|
284
|
+
|
|
285
|
+
expandedId: number | null = null;
|
|
286
|
+
detail: EmailDetail | null = null;
|
|
287
|
+
|
|
288
|
+
constructor(
|
|
289
|
+
private http: HttpClient,
|
|
290
|
+
private notify: NotificationService,
|
|
291
|
+
private route: ActivatedRoute,
|
|
292
|
+
private cdr: ChangeDetectorRef,
|
|
293
|
+
) {}
|
|
294
|
+
|
|
295
|
+
ngOnInit() {
|
|
296
|
+
const cId = this.route.snapshot.queryParamMap.get('customerId');
|
|
297
|
+
if (cId) {
|
|
298
|
+
this.lockedCustomerId = cId;
|
|
299
|
+
this.filterCustomerId = cId;
|
|
300
|
+
}
|
|
301
|
+
const oCode = this.route.snapshot.queryParamMap.get('orderCode');
|
|
302
|
+
if (oCode) this.filterOrderCode = oCode;
|
|
303
|
+
this.load();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
totalAll(): number {
|
|
307
|
+
return (this.summary.sent || 0) + (this.summary.failed || 0) + (this.summary.deferred || 0)
|
|
308
|
+
+ (this.summary.bounced || 0) + (this.summary.complained || 0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
setStatus(s: string) {
|
|
312
|
+
this.filterStatus = s;
|
|
313
|
+
this.skip = 0;
|
|
314
|
+
this.load();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clearFilters() {
|
|
318
|
+
this.filterStatus = '';
|
|
319
|
+
this.filterRecipient = '';
|
|
320
|
+
this.filterType = '';
|
|
321
|
+
this.filterCustomerId = this.lockedCustomerId || '';
|
|
322
|
+
this.filterOrderCode = '';
|
|
323
|
+
this.skip = 0;
|
|
324
|
+
this.load();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private buildParams(): string {
|
|
328
|
+
const p: string[] = [`take=${this.take}`, `skip=${this.skip}`];
|
|
329
|
+
if (this.filterStatus) p.push(`status=${encodeURIComponent(this.filterStatus)}`);
|
|
330
|
+
if (this.filterRecipient) p.push(`recipient=${encodeURIComponent(this.filterRecipient)}`);
|
|
331
|
+
if (this.filterType) p.push(`type=${encodeURIComponent(this.filterType)}`);
|
|
332
|
+
if (this.filterCustomerId) p.push(`customerId=${encodeURIComponent(this.filterCustomerId)}`);
|
|
333
|
+
if (this.filterOrderCode) p.push(`orderCode=${encodeURIComponent(this.filterOrderCode)}`);
|
|
334
|
+
return p.join('&');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
load() {
|
|
338
|
+
this.loading = true;
|
|
339
|
+
Promise.all([
|
|
340
|
+
this.http.get<any>(`/email-track/log?${this.buildParams()}`).toPromise(),
|
|
341
|
+
this.http.get<any>('/email-track/log/summary?fromDays=30').toPromise(),
|
|
342
|
+
]).then(([list, summary]) => {
|
|
343
|
+
this.rows = list?.items || [];
|
|
344
|
+
this.total = list?.total || 0;
|
|
345
|
+
this.summary = summary || this.summary;
|
|
346
|
+
this.loading = false;
|
|
347
|
+
this.expandedId = null;
|
|
348
|
+
this.detail = null;
|
|
349
|
+
this.cdr.markForCheck();
|
|
350
|
+
}).catch(() => {
|
|
351
|
+
this.loading = false;
|
|
352
|
+
this.notify.error('Failed to load email log');
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
open(id: number) {
|
|
357
|
+
if (this.expandedId === id) {
|
|
358
|
+
this.expandedId = null;
|
|
359
|
+
this.detail = null;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.expandedId = id;
|
|
363
|
+
this.detail = null;
|
|
364
|
+
this.http.get<EmailDetail>(`/email-track/log/${id}`).subscribe({
|
|
365
|
+
next: (d) => { this.detail = d; this.cdr.markForCheck(); },
|
|
366
|
+
error: () => this.notify.error('Failed to load email detail'),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
nextPage() { this.skip += this.take; this.load(); }
|
|
371
|
+
prevPage() { this.skip = Math.max(0, this.skip - this.take); this.load(); }
|
|
372
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { SharedModule, addNavMenuItem } from '@vendure/admin-ui/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registers the "Email Log" entry in the admin nav. Plugged into the
|
|
6
|
+
* existing "Customers" section so account-handling staff find it where
|
|
7
|
+
* they expect.
|
|
8
|
+
*/
|
|
9
|
+
@NgModule({
|
|
10
|
+
imports: [SharedModule],
|
|
11
|
+
providers: [
|
|
12
|
+
addNavMenuItem(
|
|
13
|
+
{
|
|
14
|
+
id: 'hulo-email-log',
|
|
15
|
+
label: 'Email Log',
|
|
16
|
+
routerLink: ['/extensions/email-log'],
|
|
17
|
+
icon: 'envelope',
|
|
18
|
+
requiresPermission: 'ReadCustomer',
|
|
19
|
+
},
|
|
20
|
+
'customers',
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
export class EmailLogNavModule {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { RouterModule } from '@angular/router';
|
|
3
|
+
import { SharedModule } from '@vendure/admin-ui/core';
|
|
4
|
+
import { FormsModule } from '@angular/forms';
|
|
5
|
+
import { HttpClientModule } from '@angular/common/http';
|
|
6
|
+
import { EmailLogComponent } from './components/email-log.component';
|
|
7
|
+
|
|
8
|
+
@NgModule({
|
|
9
|
+
imports: [
|
|
10
|
+
SharedModule, FormsModule, HttpClientModule,
|
|
11
|
+
RouterModule.forChild([
|
|
12
|
+
{ path: '', pathMatch: 'full', component: EmailLogComponent, data: { breadcrumb: 'Email Log' } },
|
|
13
|
+
]),
|
|
14
|
+
],
|
|
15
|
+
declarations: [EmailLogComponent],
|
|
16
|
+
})
|
|
17
|
+
export class EmailLogModule {}
|