-
-
+
+
+
+
Period
+
+ {#each PERIODS as p, i}
+
+ {/each}
+
+
+
+
+
+
Recurrence
+
+ {#each DAYS as day, i}
+
+ {/each}
+
+
+
+
+ {#if editRecurrence === 0}
+
+
+
+ {/if}
+
@@ -305,18 +434,15 @@
border-color: var(--color-accent);
}
- .date-time-row {
+ .field-row {
display: flex;
gap: 8px;
}
- .date-time-field {
+ .field-group {
display: flex;
flex-direction: column;
gap: 4px;
- }
-
- .date-time-field:first-child {
flex: 1;
}
@@ -328,6 +454,65 @@
color: var(--color-text-secondary);
}
+ .period-selector {
+ display: flex;
+ gap: 4px;
+ }
+
+ .period-btn {
+ flex: 1;
+ padding: 6px 8px;
+ border-radius: 6px;
+ border: 1px solid var(--color-border);
+ background: var(--color-bg-primary);
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ font-size: 11px;
+ transition: all 0.15s;
+ white-space: nowrap;
+ }
+
+ .period-btn:hover {
+ background: var(--color-bg-card-hover);
+ }
+
+ .period-btn.active {
+ border-color: var(--color-accent);
+ background: color-mix(in srgb, var(--color-accent) 15%, transparent);
+ color: var(--color-accent);
+ font-weight: 600;
+ }
+
+ .recurrence-selector {
+ display: flex;
+ gap: 3px;
+ flex-wrap: wrap;
+ }
+
+ .rec-btn {
+ padding: 5px 8px;
+ border-radius: 6px;
+ border: 1px solid var(--color-border);
+ background: var(--color-bg-primary);
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ font-size: 11px;
+ transition: all 0.15s;
+ min-width: 32px;
+ text-align: center;
+ }
+
+ .rec-btn:hover {
+ background: var(--color-bg-card-hover);
+ }
+
+ .rec-btn.active {
+ border-color: var(--color-accent);
+ background: color-mix(in srgb, var(--color-accent) 15%, transparent);
+ color: var(--color-accent);
+ font-weight: 600;
+ }
+
.task-date-input {
padding: 8px 12px;
border-radius: 8px;
@@ -342,23 +527,8 @@
position: relative;
}
- .task-time-input {
- padding: 8px 12px;
- border-radius: 8px;
- border: 1px solid var(--color-border);
- background: var(--color-bg-primary);
- color: var(--color-text-primary);
- font-size: 13px;
- outline: none;
- color-scheme: dark;
- width: 110px;
- cursor: pointer;
- position: relative;
- }
-
/* Make the native picker icon cover the entire input (Chrome/Edge only) */
- .task-date-input::-webkit-calendar-picker-indicator,
- .task-time-input::-webkit-calendar-picker-indicator {
+ .task-date-input::-webkit-calendar-picker-indicator {
position: absolute;
top: 0;
left: 0;
@@ -370,7 +540,7 @@
cursor: pointer;
}
- .task-date-input:focus, .task-time-input:focus {
+ .task-date-input:focus {
border-color: var(--color-accent);
}
@@ -430,6 +600,7 @@
display: flex;
align-items: center;
justify-content: space-between;
+ flex-wrap: wrap;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--color-border);
@@ -437,6 +608,11 @@
transition: all 0.15s;
}
+ .task-item .edit-form {
+ width: 100%;
+ margin: 0;
+ }
+
.task-item:hover {
background: var(--color-bg-card-hover);
}
@@ -485,6 +661,25 @@
color: var(--color-text-secondary);
}
+ .task-meta {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ .task-period {
+ font-size: 10px;
+ color: var(--color-text-secondary);
+ font-weight: 500;
+ }
+
+ .task-recurrence {
+ font-size: 10px;
+ color: var(--color-accent);
+ font-weight: 500;
+ }
+
.task-due {
font-size: 10px;
color: var(--color-text-secondary);
diff --git a/Provider/frontend/src/lib/api.js b/Provider/frontend/src/lib/api.js
index 46a0b0b..cd244dd 100644
--- a/Provider/frontend/src/lib/api.js
+++ b/Provider/frontend/src/lib/api.js
@@ -222,14 +222,16 @@ export async function getUpcomingTasks() {
* Create a new task.
* @param {number} userId
* @param {string} title
- * @param {number} dueDate Unix timestamp in seconds
- * @returns {Promise<{id: number, user_id: number, title: string, due_date: number, completed: boolean}>}
+ * @param {number} dueDate Unix timestamp in seconds (used for non-recurrent tasks)
+ * @param {number} period 0=Morning, 1=Afternoon, 2=Evening
+ * @param {number} recurrence 0=None, 1-7=Day of week (1=Mon)
+ * @returns {Promise<{id: number, user_id: number, title: string, due_date: number, period: number, recurrence: number, completed: boolean}>}
*/
-export async function addTask(userId, title, dueDate) {
+export async function addTask(userId, title, dueDate, period = 0, recurrence = 0) {
const res = await trackedFetch(`${API_BASE}/api/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ user_id: userId, title, due_date: dueDate })
+ body: JSON.stringify({ user_id: userId, title, due_date: dueDate, period, recurrence })
});
if (!res.ok) {
const errorText = await res.text();
@@ -241,7 +243,7 @@ export async function addTask(userId, title, dueDate) {
/**
* Update a task (partial update โ only include fields you want to change).
* @param {number} id
- * @param {Object} fields - { title?: string, due_date?: number, completed?: boolean }
+ * @param {Object} fields - { title?: string, due_date?: number, period?: number, recurrence?: number, completed?: boolean }
* @returns {Promise<{status: string}>}
*/
export async function updateTask(id, fields) {
diff --git a/Provider/frontend/version.json b/Provider/frontend/version.json
index 4f4f184..d0a7c8f 100644
--- a/Provider/frontend/version.json
+++ b/Provider/frontend/version.json
@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 1,
- "revision": 23
+ "revision": 30
}
\ No newline at end of file
diff --git a/Provider/main/api/tasks/add.cpp b/Provider/main/api/tasks/add.cpp
index 105f8f3..f78588a 100644
--- a/Provider/main/api/tasks/add.cpp
+++ b/Provider/main/api/tasks/add.cpp
@@ -1,5 +1,6 @@
// POST /api/tasks โ Create a new task
-// Body: {"user_id":1, "title":"...", "due_date":1741369200}
+// Body: {"user_id":1, "title":"...", "due_date":1741369200, "period":0,
+// "recurrence":0}
#include "cJSON.h"
#include "esp_http_server.h"
@@ -31,19 +32,26 @@ internal esp_err_t api_tasks_post_handler(httpd_req_t *req)
cJSON *user_id_item = cJSON_GetObjectItem(body, "user_id");
cJSON *title_item = cJSON_GetObjectItem(body, "title");
cJSON *due_date_item = cJSON_GetObjectItem(body, "due_date");
+ cJSON *period_item = cJSON_GetObjectItem(body, "period");
+ cJSON *recurrence_item = cJSON_GetObjectItem(body, "recurrence");
- if (!cJSON_IsNumber(user_id_item) || !cJSON_IsString(title_item) ||
- !cJSON_IsNumber(due_date_item))
+ if (!cJSON_IsNumber(user_id_item) || !cJSON_IsString(title_item))
{
cJSON_Delete(body);
- httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
- "Missing user_id, title, or due_date");
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing user_id or title");
return ESP_FAIL;
}
+ int64 due_date =
+ cJSON_IsNumber(due_date_item) ? (int64)due_date_item->valuedouble : 0;
+ uint8 period = cJSON_IsNumber(period_item) ? (uint8)period_item->valueint
+ : (uint8)PERIOD_MORNING;
+ uint8 recurrence =
+ cJSON_IsNumber(recurrence_item) ? (uint8)recurrence_item->valueint : 0;
+
task_t *task =
- add_task((uint8)user_id_item->valueint, title_item->valuestring,
- (int64)due_date_item->valuedouble);
+ add_task((uint8)user_id_item->valueint, title_item->valuestring, due_date,
+ period, recurrence);
cJSON_Delete(body);
if (!task)
@@ -58,6 +66,8 @@ internal esp_err_t api_tasks_post_handler(httpd_req_t *req)
cJSON_AddNumberToObject(resp, "user_id", task->user_id);
cJSON_AddStringToObject(resp, "title", task->title);
cJSON_AddNumberToObject(resp, "due_date", (double)task->due_date);
+ cJSON_AddNumberToObject(resp, "period", task->period);
+ cJSON_AddNumberToObject(resp, "recurrence", task->recurrence);
cJSON_AddBoolToObject(resp, "completed", task->completed);
const char *json = cJSON_PrintUnformatted(resp);
diff --git a/Provider/main/api/tasks/list.cpp b/Provider/main/api/tasks/list.cpp
index 8104981..d2d378b 100644
--- a/Provider/main/api/tasks/list.cpp
+++ b/Provider/main/api/tasks/list.cpp
@@ -41,6 +41,8 @@ internal esp_err_t api_tasks_get_handler(httpd_req_t *req)
cJSON_AddNumberToObject(obj, "user_id", g_Tasks[i].user_id);
cJSON_AddStringToObject(obj, "title", g_Tasks[i].title);
cJSON_AddNumberToObject(obj, "due_date", (double)g_Tasks[i].due_date);
+ cJSON_AddNumberToObject(obj, "period", g_Tasks[i].period);
+ cJSON_AddNumberToObject(obj, "recurrence", g_Tasks[i].recurrence);
cJSON_AddBoolToObject(obj, "completed", g_Tasks[i].completed);
cJSON_AddItemToArray(arr, obj);
}
diff --git a/Provider/main/api/tasks/store.cpp b/Provider/main/api/tasks/store.cpp
index 4aef8ab..b5d94d4 100644
--- a/Provider/main/api/tasks/store.cpp
+++ b/Provider/main/api/tasks/store.cpp
@@ -19,7 +19,8 @@ task_t *find_task(uint16 id)
}
// Add a task, returns pointer to new task or nullptr if full
-task_t *add_task(uint8 user_id, const char *title, int64 due_date)
+task_t *add_task(uint8 user_id, const char *title, int64 due_date, uint8 period,
+ uint8 recurrence)
{
// Verify user exists
if (find_user(user_id) == nullptr)
@@ -35,6 +36,8 @@ task_t *add_task(uint8 user_id, const char *title, int64 due_date)
g_Tasks[i].user_id = user_id;
strlcpy(g_Tasks[i].title, title, sizeof(g_Tasks[i].title));
g_Tasks[i].due_date = due_date;
+ g_Tasks[i].period = period;
+ g_Tasks[i].recurrence = recurrence;
g_Tasks[i].completed = false;
g_Tasks[i].active = true;
return &g_Tasks[i];
@@ -94,18 +97,19 @@ void seed_tasks()
{
int64 now = (int64)(esp_timer_get_time() / 1000000);
- // Alice's tasks (user_id = 1)
- add_task(1, "Buy groceries", now + 86400); // +1 day
- add_task(1, "Review PR #42", now + 3600); // +1 hour
- add_task(1, "Book dentist appointment", now + 172800); // +2 days
- add_task(1, "Update resume", now + 604800); // +7 days
+ // Alice's tasks (user_id = 1) โ mix of one-off and recurring
+ add_task(1, "Buy groceries", now + 86400, PERIOD_MORNING);
+ add_task(1, "Review PR #42", now + 3600, PERIOD_AFTERNOON);
+ add_task(1, "Book dentist appointment", now + 172800, PERIOD_MORNING);
+ add_task(1, "Update resume", now + 604800, PERIOD_EVENING);
- // Bob's tasks (user_id = 2)
- add_task(2, "Fix login bug", now + 7200); // +2 hours
- add_task(2, "Deploy staging", now + 43200); // +12 hours
- add_task(2, "Write unit tests", now + 259200); // +3 days
+ // Bob's tasks (user_id = 2) โ some recurring routines
+ add_task(2, "Morning standup", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
+ add_task(2, "Deploy staging", now + 43200, PERIOD_AFTERNOON);
+ add_task(2, "Write unit tests", now + 259200, PERIOD_MORNING);
- // Charlie's tasks (user_id = 3)
- add_task(3, "Water plants", now + 1800); // +30 min
- add_task(3, "Call plumber", now + 86400); // +1 day
+ // Charlie's tasks (user_id = 3) โ kid routine examples
+ add_task(3, "Breakfast", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
+ add_task(3, "Homework", 0, PERIOD_AFTERNOON, 0x15); // Mon+Wed+Fri
+ add_task(3, "Bath time", 0, PERIOD_EVENING, 0x7F); // Every day
}
diff --git a/Provider/main/api/tasks/store.hpp b/Provider/main/api/tasks/store.hpp
index d456c38..5342809 100644
--- a/Provider/main/api/tasks/store.hpp
+++ b/Provider/main/api/tasks/store.hpp
@@ -5,7 +5,8 @@
// Data store operations for tasks
task_t *find_task(uint16 id);
-task_t *add_task(uint8 user_id, const char *title, int64 due_date);
+task_t *add_task(uint8 user_id, const char *title, int64 due_date,
+ uint8 period = PERIOD_MORNING, uint8 recurrence = 0);
bool remove_task(uint16 id);
void remove_tasks_for_user(uint8 user_id);
void sort_tasks_by_due_date(task_t **arr, int count);
diff --git a/Provider/main/api/tasks/upcoming.cpp b/Provider/main/api/tasks/upcoming.cpp
index 4c4e74b..812c972 100644
--- a/Provider/main/api/tasks/upcoming.cpp
+++ b/Provider/main/api/tasks/upcoming.cpp
@@ -1,4 +1,4 @@
-// GET /api/tasks/upcoming โ Top 3 upcoming tasks per user (for Dashboard)
+// GET /api/tasks/upcoming โ Today's tasks per user, grouped by period
#include "cJSON.h"
#include "esp_http_server.h"
@@ -20,36 +20,26 @@ internal esp_err_t api_tasks_upcoming_handler(httpd_req_t *req)
continue;
// Collect incomplete tasks for this user
- task_t *user_tasks[MAX_TASKS];
- int count = 0;
-
- for (int t = 0; t < MAX_TASKS; t++)
- {
- if (g_Tasks[t].active && g_Tasks[t].user_id == g_Users[u].id &&
- !g_Tasks[t].completed)
- {
- user_tasks[count++] = &g_Tasks[t];
- }
- }
-
- // Sort by due_date ascending
- sort_tasks_by_due_date(user_tasks, count);
-
- // Build user object with top 3
+ // Include: recurring tasks (any day) and one-off tasks
cJSON *user_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(user_obj, "id", g_Users[u].id);
cJSON_AddStringToObject(user_obj, "name", g_Users[u].name);
cJSON *tasks_arr = cJSON_AddArrayToObject(user_obj, "tasks");
- int limit = count < 3 ? count : 3;
- for (int i = 0; i < limit; i++)
+
+ for (int t = 0; t < MAX_TASKS; t++)
{
+ if (!g_Tasks[t].active || g_Tasks[t].completed ||
+ g_Tasks[t].user_id != g_Users[u].id)
+ continue;
+
cJSON *t_obj = cJSON_CreateObject();
- cJSON_AddNumberToObject(t_obj, "id", user_tasks[i]->id);
- cJSON_AddStringToObject(t_obj, "title", user_tasks[i]->title);
- cJSON_AddNumberToObject(t_obj, "due_date",
- (double)user_tasks[i]->due_date);
- cJSON_AddBoolToObject(t_obj, "completed", user_tasks[i]->completed);
+ cJSON_AddNumberToObject(t_obj, "id", g_Tasks[t].id);
+ cJSON_AddStringToObject(t_obj, "title", g_Tasks[t].title);
+ cJSON_AddNumberToObject(t_obj, "due_date", (double)g_Tasks[t].due_date);
+ cJSON_AddNumberToObject(t_obj, "period", g_Tasks[t].period);
+ cJSON_AddNumberToObject(t_obj, "recurrence", g_Tasks[t].recurrence);
+ cJSON_AddBoolToObject(t_obj, "completed", g_Tasks[t].completed);
cJSON_AddItemToArray(tasks_arr, t_obj);
}
diff --git a/Provider/main/api/tasks/update.cpp b/Provider/main/api/tasks/update.cpp
index 6ef59ce..ac898bf 100644
--- a/Provider/main/api/tasks/update.cpp
+++ b/Provider/main/api/tasks/update.cpp
@@ -1,6 +1,6 @@
// POST /api/tasks/update โ Modify a task
-// Body: {"id":1, "title":"...", "due_date":..., "completed":true}
-// All fields except "id" are optional
+// Body: {"id":1, "title":"...", "due_date":..., "period":0, "recurrence":0,
+// "completed":true} All fields except "id" are optional
#include "cJSON.h"
#include "esp_http_server.h"
@@ -58,6 +58,18 @@ internal esp_err_t api_tasks_update_handler(httpd_req_t *req)
task->due_date = (int64)due_date_item->valuedouble;
}
+ cJSON *period_item = cJSON_GetObjectItem(body, "period");
+ if (cJSON_IsNumber(period_item))
+ {
+ task->period = (uint8)period_item->valueint;
+ }
+
+ cJSON *recurrence_item = cJSON_GetObjectItem(body, "recurrence");
+ if (cJSON_IsNumber(recurrence_item))
+ {
+ task->recurrence = (uint8)recurrence_item->valueint;
+ }
+
cJSON *completed_item = cJSON_GetObjectItem(body, "completed");
if (cJSON_IsBool(completed_item))
{
diff --git a/Provider/main/todo.hpp b/Provider/main/todo.hpp
index 01582ee..c4ac578 100644
--- a/Provider/main/todo.hpp
+++ b/Provider/main/todo.hpp
@@ -5,14 +5,24 @@
#include "types.hpp"
#include "user.hpp"
+enum task_period_t : uint8
+{
+ PERIOD_MORNING = 0x01, // bit 0
+ PERIOD_AFTERNOON = 0x02, // bit 1
+ PERIOD_EVENING = 0x04, // bit 2
+ PERIOD_ALL_DAY = 0x07 // all bits
+};
+
struct task_t
{
- uint16 id; // Auto-assigned (1โ65535, 0 = empty slot)
- uint8 user_id; // Owner (matches user_t.id)
- char title[64]; // Task description
- int64 due_date; // Unix timestamp (seconds)
- bool completed; // Done flag
- bool active; // Slot in use
+ char title[64]; // Task description
+ int64 due_date; // Unix timestamp (seconds) - used when recurrence is 0
+ uint16 id; // Auto-assigned (1โ65535, 0 = empty slot)
+ uint8 user_id; // Owner (matches user_t.id)
+ uint8 recurrence; // Bitmask: bit0=Mon, bit1=Tue, ..., bit6=Sun. 0=none
+ uint8 period : 3; // Bitmask: bit0=Morning, bit1=Afternoon, bit2=Evening
+ bool completed : 1; // Done flag
+ bool active : 1; // Slot in use
};
constexpr int MAX_TASKS = 32;