diff --git a/.gitignore b/.gitignore index a5c6fe7..989d5c0 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,6 @@ external/* # Agent Tasks Provider/AgentTasks/ + +# OTA files +*.bundle \ No newline at end of file diff --git a/Provider/frontend/.env b/Provider/frontend/.env index 808554a..77b15ed 100644 --- a/Provider/frontend/.env +++ b/Provider/frontend/.env @@ -1,2 +1,2 @@ -VITE_API_BASE=http://192.168.50.216 +VITE_API_BASE=http://calendink.local/ MKLITTLEFS_PATH=W:\Classified\Calendink\Provider\build\littlefs_py_venv\Scripts\littlefs-python.exe diff --git a/Provider/frontend/scripts/deploy.js b/Provider/frontend/scripts/deploy.js index a1d9cd9..d6d8300 100644 --- a/Provider/frontend/scripts/deploy.js +++ b/Provider/frontend/scripts/deploy.js @@ -32,7 +32,7 @@ function loadEnv() { loadEnv(); -const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '') : null; +const targetIP = process.env.VITE_API_BASE ? process.env.VITE_API_BASE.replace('http://', '').replace(/\/+$/, '') : null; if (!targetIP) { console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.'); diff --git a/Provider/frontend/src/App.svelte b/Provider/frontend/src/App.svelte index e6f2c9c..f9041d5 100644 --- a/Provider/frontend/src/App.svelte +++ b/Provider/frontend/src/App.svelte @@ -75,6 +75,8 @@ } let isFetching = false; + let lastKnownFirmware = null; + let lastKnownSlot = null; async function fetchAll(silent = false) { if (isFetching) return; isFetching = true; @@ -85,6 +87,16 @@ getOTAStatus(), getUpcomingTasks().catch(() => ({ users: [] })) ]); + // Detect any OTA update: firmware version change OR www partition flip + const fwChanged = lastKnownFirmware && sys.firmware !== lastKnownFirmware; + const slotChanged = lastKnownSlot !== null && ota.active_slot !== lastKnownSlot; + if (fwChanged || slotChanged) { + console.log(`OTA detected (fw: ${fwChanged}, slot: ${slotChanged}). Reloading...`); + window.location.href = window.location.pathname + '?t=' + Date.now(); + return; + } + lastKnownFirmware = sys.firmware; + lastKnownSlot = ota.active_slot; systemInfo = sys; otaStatus = ota; upcomingData = upcoming; @@ -180,7 +192,7 @@
-

Calendink Provider ๐Ÿ“…๐Ÿ“…๐Ÿš€๐Ÿš€โšกโœจ๐ŸŒˆ

+

Calendink Provider ๐Ÿ“…โšกโœจ๐ŸŒˆ

ESP32-S3 System Dashboard v{__APP_VERSION__}

@@ -214,29 +226,46 @@ {#if upcomingData.users.length > 0} + {@const periodNames = ['Morning', 'Afternoon', 'Evening']} + {@const periodIcons = ['๐ŸŒ…', 'โ˜€๏ธ', '๐ŸŒ™']}

- ๐Ÿ“‹ Upcoming Tasks + ๐Ÿ“‹ Today's Routine

{#each upcomingData.users as user} + {@const routineTasks = user.tasks.filter(t => t.recurrence > 0)}

{user.name}

- {#if user.tasks.length === 0} -

No pending tasks

+ {#if routineTasks.length === 0} +

No routine tasks

{:else} -
- {#each user.tasks as task} -
- - {formatRelativeDate(task.due_date)} - - {task.title} + {#each [0, 1, 2] as periodIdx} + {@const tasksForPeriod = routineTasks.filter(t => t.period & (1 << periodIdx))} + {#if tasksForPeriod.length > 0} +
+
+ {periodIcons[periodIdx]} {periodNames[periodIdx]} +
+ {#each tasksForPeriod as task} +
+ โ€ข {task.title} + {#if task.recurrence > 0} + + {task.recurrence === 0x7F ? 'โˆž' : task.recurrence === 0x1F ? 'wk' : ''} + + {:else if task.due_date > 0} + + {formatRelativeDate(task.due_date)} + + {/if} +
+ {/each}
- {/each} -
+ {/if} + {/each} {/if}
{/each} diff --git a/Provider/frontend/src/lib/TaskManager.svelte b/Provider/frontend/src/lib/TaskManager.svelte index cb0fbd9..d0dde47 100644 --- a/Provider/frontend/src/lib/TaskManager.svelte +++ b/Provider/frontend/src/lib/TaskManager.svelte @@ -6,17 +6,61 @@ let tasks = $state([]); let error = $state(''); + // Period and day-of-week labels + const PERIODS = ['Morning', 'Afternoon', 'Evening']; + const PERIOD_ICONS = ['๐ŸŒ…', 'โ˜€๏ธ', '๐ŸŒ™']; + const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + // Shared bitmask helpers (used for both period and recurrence) + function toggleBit(current, idx) { + return current ^ (1 << idx); + } + + function hasBit(mask, idx) { + return (mask & (1 << idx)) !== 0; + } + + function formatPeriod(mask) { + if (mask === 0x07) return 'All Day'; + const names = []; + for (let i = 0; i < 3; i++) { + if (mask & (1 << i)) names.push(PERIODS[i]); + } + return names.length > 0 ? names.join(', ') : 'None'; + } + + function periodIcons(mask) { + const icons = []; + for (let i = 0; i < 3; i++) { + if (mask & (1 << i)) icons.push(PERIOD_ICONS[i]); + } + return icons.join(''); + } + + function formatRecurrence(mask) { + if (mask === 0x7F) return 'Every day'; + if (mask === 0x1F) return 'Weekdays'; + if (mask === 0x60) return 'Weekends'; + const names = []; + for (let i = 0; i < 7; i++) { + if (mask & (1 << i)) names.push(DAYS[i]); + } + return names.join(', '); + } + // Add task form state let newTitle = $state(''); + let newPeriod = $state(0x01); + let newRecurrence = $state(0); let newDueDay = $state(''); - let newDueTime = $state(''); let showAddForm = $state(false); // Edit state let editingTaskId = $state(null); let editTitle = $state(''); + let editPeriod = $state(0); + let editRecurrence = $state(0); let editDueDay = $state(''); - let editDueTime = $state(''); // Confirm delete let confirmDeleteId = $state(null); @@ -28,8 +72,13 @@ } try { tasks = await getTasks(selectedUserId); - // Sort by due_date ascending - tasks.sort((a, b) => a.due_date - b.due_date); + // Sort: recurrent first (by day), then one-off by due_date + tasks.sort((a, b) => { + if (a.recurrence && !b.recurrence) return -1; + if (!a.recurrence && b.recurrence) return 1; + if (a.recurrence && b.recurrence) return a.recurrence - b.recurrence; + return a.due_date - b.due_date; + }); error = ''; } catch (e) { error = e.message; @@ -47,14 +96,20 @@ async function handleAddTask(e) { e.preventDefault(); - if (!newTitle.trim() || !newDueDay || !newDueTime) return; + if (!newTitle.trim()) return; + // For non-recurrent tasks, require a date + if (newRecurrence === 0 && !newDueDay) return; + + const dueTimestamp = newRecurrence > 0 + ? 0 + : Math.floor(new Date(`${newDueDay}T00:00`).getTime() / 1000); - const dueTimestamp = Math.floor(new Date(`${newDueDay}T${newDueTime}`).getTime() / 1000); try { - await addTask(selectedUserId, newTitle.trim(), dueTimestamp); + await addTask(selectedUserId, newTitle.trim(), dueTimestamp, newPeriod, newRecurrence); newTitle = ''; + newPeriod = 0x01; + newRecurrence = 0; newDueDay = ''; - newDueTime = ''; showAddForm = false; await fetchTasks(); } catch (e) { @@ -74,20 +129,34 @@ function startEditing(task) { editingTaskId = task.id; editTitle = task.title; - const parts = formatDateForInput(task.due_date); - editDueDay = parts.day; - editDueTime = parts.time; + editPeriod = task.period; + editRecurrence = task.recurrence; + if (task.recurrence === 0 && task.due_date) { + const d = new Date(task.due_date * 1000); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const dayNum = String(d.getDate()).padStart(2, '0'); + editDueDay = `${year}-${month}-${dayNum}`; + } else { + editDueDay = ''; + } } async function saveEdit(e) { e.preventDefault(); if (!editTitle.trim()) return; + if (editRecurrence === 0 && !editDueDay) return; + + const dueTimestamp = editRecurrence > 0 + ? 0 + : Math.floor(new Date(`${editDueDay}T00:00`).getTime() / 1000); - const dueTimestamp = Math.floor(new Date(`${editDueDay}T${editDueTime}`).getTime() / 1000); try { await updateTask(editingTaskId, { title: editTitle.trim(), - due_date: dueTimestamp + due_date: dueTimestamp, + period: editPeriod, + recurrence: editRecurrence }); editingTaskId = null; await fetchTasks(); @@ -106,20 +175,12 @@ } } - function formatDateForInput(timestamp) { - const d = new Date(timestamp * 1000); + function setTodayForNewTask() { + const d = new Date(); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const dayNum = String(d.getDate()).padStart(2, '0'); - const hours = String(d.getHours()).padStart(2, '0'); - const mins = String(d.getMinutes()).padStart(2, '0'); - return { day: `${year}-${month}-${dayNum}`, time: `${hours}:${mins}` }; - } - - function setNowForNewTask() { - const parts = formatDateForInput(Date.now() / 1000); - newDueDay = parts.day; - newDueTime = parts.time; + newDueDay = `${year}-${month}-${dayNum}`; } function formatRelativeDate(timestamp) { @@ -139,8 +200,9 @@ return diff < 0 ? `${days}d ago` : `in ${days}d`; } - function isOverdue(timestamp) { - return timestamp < Date.now() / 1000; + function isOverdue(task) { + if (task.recurrence > 0) return false; // Recurrent tasks don't have overdue + return task.due_date < Date.now() / 1000; } @@ -151,7 +213,7 @@

Tasks

{#if !showAddForm} - + {/if}
@@ -165,39 +227,99 @@ class="task-input" autofocus /> -
- - + +
+
+ Period +
+ {#each PERIODS as p, i} + + {/each} +
+
+ +
+
+ Recurrence +
+ {#each DAYS as day, i} + + {/each} +
+
+
+ + {#if newRecurrence === 0} +
+ +
+ {/if} +
- - + +
{/if}
{#each tasks as task} -
+
{#if editingTaskId === task.id}
-
- - + +
+
+ Period +
+ {#each PERIODS as p, i} + + {/each} +
+
+ +
+
+ Recurrence +
+ {#each DAYS as day, i} + + {/each} +
+
+
+ + {#if editRecurrence === 0} +
+ +
+ {/if} +
@@ -213,9 +335,16 @@ />
{task.title} - - {formatRelativeDate(task.due_date)} - +
+ {periodIcons(task.period)} {formatPeriod(task.period)} + {#if task.recurrence > 0} + ๐Ÿ” {formatRecurrence(task.recurrence)} + {:else} + + {formatRelativeDate(task.due_date)} + + {/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;