Skip to content

Commit 6b7b4c9

Browse files
committed
Scheduled emails to time, Design, Stats, Api
Breaking - DB update.
1 parent 085b7b2 commit 6b7b4c9

File tree

14 files changed

+435
-123
lines changed

14 files changed

+435
-123
lines changed

assets/js/flows.js

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
let
42
globalFlowID,
53
globalFlowStepsData = [];
@@ -27,7 +25,7 @@ function addFlow() {
2725
attrs: {
2826
class: 'forinput'
2927
},
30-
children: ['List name']
28+
children: ['Flow name']
3129
}),
3230
jsCreateElement('input', {
3331
attrs: {
@@ -240,6 +238,13 @@ async function buildFlowHTML(data) {
240238
},
241239
children: ['Click link in previous mail']
242240
}));
241+
triggerOpts.push(jsCreateElement('option', {
242+
attrs: {
243+
value: 'time',
244+
selected: (step.trigger_type == 'time') ? "selected" : false
245+
},
246+
children: ['Specific time']
247+
}));
243248

244249

245250
//
@@ -348,7 +353,16 @@ async function buildFlowHTML(data) {
348353
jsCreateElement('input', {
349354
attrs: {
350355
id: "flowStepDelay_" + step.id,
351-
value: step.delay_minutes
356+
value: step.delay_minutes,
357+
style: step.trigger_type == 'time' ? 'display: none;' : ''
358+
},
359+
}),
360+
jsCreateElement('input', {
361+
attrs: {
362+
id: "flowStepTime_" + step.id,
363+
type: 'time',
364+
value: step.scheduled_time || '',
365+
style: step.trigger_type == 'time' ? '' : 'display: none;'
352366
},
353367
})
354368
]
@@ -401,6 +415,20 @@ async function buildFlowHTML(data) {
401415
let inputs = document.querySelectorAll("input, select");
402416
for (let i = 0; i < inputs.length; i++) {
403417
inputs[i].addEventListener("change", function() {
418+
// Check if this is a trigger type selection
419+
if (this.id.startsWith("flowStepTrigger_")) {
420+
const stepId = this.id.split("_")[1];
421+
// Show/hide time or delay input based on trigger type
422+
if (this.value === "time") {
423+
document.getElementById("flowStepTime_" + stepId).style.display = "";
424+
document.getElementById("flowStepDelay_" + stepId).style.display = "none";
425+
} else {
426+
document.getElementById("flowStepTime_" + stepId).style.display = "none";
427+
document.getElementById("flowStepDelay_" + stepId).style.display = "";
428+
}
429+
}
430+
431+
// Call updateFlowStep for all changes
404432
updateFlowStep(this.id.split("_")[1]);
405433
});
406434
}
@@ -535,6 +563,12 @@ async function addFlowStep() {
535563
value: 'click'
536564
},
537565
children: ['Click link in previous mail']
566+
}),
567+
jsCreateElement('option', {
568+
attrs: {
569+
value: 'time'
570+
},
571+
children: ['Specific time']
538572
})
539573
]
540574
})
@@ -559,11 +593,19 @@ async function addFlowStep() {
559593
min: '10'
560594
}
561595
}),
596+
jsCreateElement('input', {
597+
attrs: {
598+
type: 'time',
599+
id: 'flowStepTime',
600+
value: '',
601+
style: 'display: none;'
602+
}
603+
}),
562604
jsCreateElement('div', {
563605
attrs: {
564606
style: "font-size: 12px;"
565607
},
566-
children: ['Minimum time is 10 minutes. Emails will be scheduled as soon as the step is added.']
608+
children: ['Minimum time is 2 minutes. Emails will be scheduled as soon as the step is added.']
567609
})
568610
]
569611
}),
@@ -586,6 +628,17 @@ async function addFlowStep() {
586628
labelFloater();
587629
setTimeout(() => {
588630
dqs("#flowStepName").focus();
631+
632+
// Set the trigger type to delay by default
633+
dqs("#flowStepTrigger").addEventListener("change", function() {
634+
if (this.value === "time") {
635+
dqs("#flowStepTime").style.display = "";
636+
dqs("#flowStepDelay").style.display = "none";
637+
} else {
638+
dqs("#flowStepTime").style.display = "none";
639+
dqs("#flowStepDelay").style.display = "";
640+
}
641+
});
589642
}, 100);
590643
}
591644

@@ -596,7 +649,8 @@ function addFlowStepDo() {
596649
mailID = dqs("#flowStepMail").value,
597650
subject = dqs("#flowStepSubject").value,
598651
trigger = dqs("#flowStepTrigger").value,
599-
delay = dqs("#flowStepDelay").value;
652+
delay = dqs("#flowStepDelay").value,
653+
time = dqs("#flowStepTime").value;
600654

601655
fetch("/api/flow_steps/create", {
602656
method: "POST",
@@ -606,7 +660,8 @@ function addFlowStepDo() {
606660
mailID: mailID,
607661
subject: subject,
608662
trigger: trigger,
609-
delay: delay
663+
delay: delay,
664+
scheduledTime: time
610665
})
611666
})
612667
.then(manageErrors)
@@ -623,7 +678,8 @@ function updateFlowStep(flowStepID) {
623678
mailID = dqs("#flowStepMail_" + flowStepID).value,
624679
delayMinutes = dqs("#flowStepDelay_" + flowStepID).value,
625680
subject = dqs("#flowStepSubject_" + flowStepID).value,
626-
triggerType = dqs("#flowStepTrigger_" + flowStepID).value;
681+
triggerType = dqs("#flowStepTrigger_" + flowStepID).value,
682+
scheduledTime = dqs("#flowStepTime_" + flowStepID).value;
627683

628684
fetch("/api/flow_steps/update", {
629685
method: "POST",
@@ -632,7 +688,8 @@ function updateFlowStep(flowStepID) {
632688
mailID: mailID,
633689
delayMinutes: delayMinutes,
634690
subject: subject,
635-
trigger: triggerType
691+
trigger: triggerType,
692+
scheduledTime: scheduledTime
636693
})
637694
})
638695
.then(manageErrors);

nimletter.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

3-
version = "0.4.8"
3+
version = "0.5.0"
44
author = "ThomasTJdev"
55
description = "Newsletter"
66
license = "AGPL v3"

src/database/db_schema.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS flow_steps (
3636
step_number INT NOT NULL, -- Sequence of steps in the flow
3737
trigger_type TEXT DEFAULT 'delay', -- open, linkclick, delay, etc.
3838
delay_minutes INT NOT NULL DEFAULT 0, -- Delay from the previous step
39+
scheduled_time TIME DEFAULT NULL, -- Specific time to schedule the email (in GMT0)
3940
name TEXT NOT NULL, -- Name of the step
4041
subject TEXT NOT NULL, -- Email subject for this step
4142
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

src/html/dash.nimf

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,35 @@ curl -X POST \
252252
http://localhost:5555/api/event
253253
</pre>
254254
</div>
255-
</div>
256-
257-
</div>
255+
<div>
256+
<b>Add contact to list:</b>
257+
<pre>
258+
curl -X POST \
259+
-H "Content-Type: application/json" \
260+
-H "Authorization: Bearer YOUR_API_KEY" \
261+
-d '{
262+
"event": "contact-add-to-list",
263+
"email": "mail@nimletter.com",
264+
"list": "list-identifier",
265+
"flowStep": 1
266+
}' \
267+
http://localhost:5555/api/event
268+
</pre>
269+
</div>
270+
<div>
271+
<b>Remove contact from list:</b>
272+
<pre>
273+
curl -X POST \
274+
-H "Content-Type: application/json" \
275+
-H "Authorization: Bearer YOUR_API_KEY" \
276+
-d '{
277+
"event": "contact-remove-from-list",
278+
"email": "mail@nimletter.com",
279+
"list": "list-identifier"
280+
}' \
281+
http://localhost:5555/api/event
282+
</pre>
283+
</div>
258284
</div>
259285
</main>
260286
</body>

src/html/sidebar.nimf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
Logout
9191
</div>
9292
</a>
93+
<div style=" width: 100%; height: 1px; border-top: 1px solid var(--colorN30); margin: 20px 0; "></div>
94+
<div style="font-size: 12px; color: var(--colorN200);">
95+
Server time: ${now()}
96+
</div>
9397
</div>
9498
</div>
9599
#end proc

src/routes/routes_analytics.nim

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ proc(request: Request) =
2626
if not c.loggedIn: resp Http401
2727

2828
var data: seq[seq[string]]
29+
var dataPending: seq[seq[string]]
2930
var openData: seq[seq[string]]
3031
var clickData: seq[seq[string]]
3132
pg.withConnection conn:
@@ -46,6 +47,20 @@ proc(request: Request) =
4647
"""
4748
))
4849

50+
dataPending = getAllRows(conn, sqlSelect(
51+
table = "pending_emails",
52+
select = [
53+
"to_char(date_trunc('day', scheduled_for), 'YYYY-MM-DD') AS day",
54+
"COUNT(*) FILTER (WHERE status = 'pending') AS pending"
55+
],
56+
customSQL = """
57+
WHERE scheduled_for >= current_date - interval '20 days'
58+
AND scheduled_for <= current_date + interval '7 days'
59+
GROUP BY day
60+
ORDER BY day
61+
"""
62+
))
63+
4964
openData = getAllRows(conn, sqlSelect(
5065
table = "email_opens",
5166
select = [
@@ -101,11 +116,16 @@ proc(request: Request) =
101116
for row in data:
102117
let idx = days.find(row[0])
103118
if idx >= 0:
104-
pending[idx] = row[1].parseInt
119+
#pending[idx] = row[1].parseInt
105120
sent[idx] = row[2].parseInt
106121
bounced[idx] = row[3].parseInt
107122
complained[idx] = row[4].parseInt
108123

124+
for row in dataPending:
125+
let idx = days.find(row[0])
126+
if idx >= 0:
127+
pending[idx] = row[1].parseInt
128+
109129
for row in openData:
110130
let idx = days.find(row[0])
111131
if idx >= 0:

src/routes/routes_contacts.nim

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -276,61 +276,9 @@ proc(request: Request) =
276276
listID = @"listID"
277277
listType = @"listType"
278278

279-
if not userID.isValidInt():
280-
resp Http400, "Invalid user ID"
281-
282-
if not listID.isValidInt():
283-
resp Http400, "Invalid list ID"
284-
285-
if listType == "pending":
286-
# Remove int from array pendinglists
287-
pg.withConnection conn:
288-
exec(conn, sqlUpdate(
289-
table = "contacts",
290-
data = [
291-
"pending_lists = array_remove(pending_lists, ?)",
292-
"updated_at",
293-
],
294-
where = ["id = ?"]
295-
),
296-
listID,
297-
$now().utc,
298-
userID
299-
)
300-
301-
resp Http200
302-
#
303-
# Get potential flow, so we can stop pending emails
304-
#
305-
pg.withConnection conn:
306-
let flowIDs = getValue(conn, sqlSelect(
307-
table = "lists",
308-
select = ["array_to_string(lists.flow_ids, ',') as flows"],
309-
where = ["id = ?"]
310-
), listID)
311-
312-
if flowIDs != "":
313-
for flowID in flowIDs.split(","):
314-
exec(conn, sqlUpdate(
315-
table = "pending_emails",
316-
data = [
317-
"status = 'cancelled'",
318-
"scheduled_for = NULL",
319-
"updated_at = ?"
320-
],
321-
where = [
322-
"user_id = ?",
323-
"flow_id = ?",
324-
"status = 'pending'"
325-
]
326-
),
327-
$now().utc, userID, flowID)
328-
329-
exec(conn, sqlDelete(
330-
table = "subscriptions",
331-
where = ["user_id = ?", "list_id = ?"]),
332-
userID, listID
333-
)
279+
let data = contactRemoveFromList(userID, listID, listType)
280+
if not data.success:
281+
resp Http400, data.msg
334282

335283
resp Http200
336284
)

src/routes/routes_event.nim

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import
1616
../database/database_connection,
1717
../utils/auth,
1818
../utils/contacts_utils,
19+
../utils/list_utils,
1920
../utils/validate_data
2021

2122

@@ -216,6 +217,47 @@ proc(request: Request) =
216217
resp Http200, data.data
217218

218219

220+
of "contact-add-to-list":
221+
let userID = getUserIDfromEmail(email)
222+
if userID == "":
223+
resp Http400, "User not found"
224+
let listID = listIDfromIdentifier(if data.hasKey("list"): data["list"].getStr() else: "")
225+
var flowStep = if data.hasKey("flowStep"): data["flowStep"].getInt() else: 1
226+
if listID == "":
227+
resp Http400, "List identifier required"
228+
if flowStep == 0:
229+
flowStep = 1
230+
231+
let data = addContactToList(userID, listID, flowStep)
232+
if not data:
233+
resp Http400, %* {
234+
"success": false,
235+
"message": "Failed to add contact to list"
236+
}
237+
238+
resp Http200, %* {
239+
"success": true,
240+
"message": "Contact added to list"
241+
}
242+
243+
244+
of "contact-remove-from-list":
245+
let userID = getUserIDfromEmail(email)
246+
if userID == "":
247+
resp Http400, "User not found"
248+
let listID = listIDfromIdentifier(if data.hasKey("list"): data["list"].getStr() else: "")
249+
if listID == "":
250+
resp Http400, "List identifier required"
251+
252+
let data = contactRemoveFromList(userID, listID, "")
253+
if not data.success:
254+
resp Http400, %* {
255+
"success": false,
256+
"message": data.msg
257+
}
258+
259+
resp Http200, data.data
260+
219261
else:
220262
resp Http400, "Invalid event"
221263

0 commit comments

Comments
 (0)