Updated the task view and backend to handle more like a routine
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -93,3 +93,6 @@ external/*
|
|||||||
|
|
||||||
# Agent Tasks
|
# Agent Tasks
|
||||||
Provider/AgentTasks/
|
Provider/AgentTasks/
|
||||||
|
|
||||||
|
# OTA files
|
||||||
|
*.bundle
|
||||||
@@ -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
|
MKLITTLEFS_PATH=W:\Classified\Calendink\Provider\build\littlefs_py_venv\Scripts\littlefs-python.exe
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function loadEnv() {
|
|||||||
|
|
||||||
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) {
|
if (!targetIP) {
|
||||||
console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.');
|
console.error('Error: VITE_API_BASE not found in frontend/.env. Please set it to the target device IP.');
|
||||||
|
|||||||
@@ -75,6 +75,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isFetching = false;
|
let isFetching = false;
|
||||||
|
let lastKnownFirmware = null;
|
||||||
|
let lastKnownSlot = null;
|
||||||
async function fetchAll(silent = false) {
|
async function fetchAll(silent = false) {
|
||||||
if (isFetching) return;
|
if (isFetching) return;
|
||||||
isFetching = true;
|
isFetching = true;
|
||||||
@@ -85,6 +87,16 @@
|
|||||||
getOTAStatus(),
|
getOTAStatus(),
|
||||||
getUpcomingTasks().catch(() => ({ users: [] }))
|
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;
|
systemInfo = sys;
|
||||||
otaStatus = ota;
|
otaStatus = ota;
|
||||||
upcomingData = upcoming;
|
upcomingData = upcoming;
|
||||||
@@ -180,7 +192,7 @@
|
|||||||
<div class="w-full max-w-6xl mx-auto space-y-8">
|
<div class="w-full max-w-6xl mx-auto space-y-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅📅🚀🚀⚡✨🌈</h1>
|
<h1 class="text-2xl font-bold text-accent">Calendink Provider 📅⚡✨🌈</h1>
|
||||||
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
|
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
|
||||||
|
|
||||||
<!-- Status Badge -->
|
<!-- Status Badge -->
|
||||||
@@ -214,30 +226,47 @@
|
|||||||
|
|
||||||
<!-- Upcoming Tasks Section (top priority) -->
|
<!-- Upcoming Tasks Section (top priority) -->
|
||||||
{#if upcomingData.users.length > 0}
|
{#if upcomingData.users.length > 0}
|
||||||
|
{@const periodNames = ['Morning', 'Afternoon', 'Evening']}
|
||||||
|
{@const periodIcons = ['🌅', '☀️', '🌙']}
|
||||||
<div class="bg-bg-card border border-border rounded-xl overflow-hidden shadow-xl">
|
<div class="bg-bg-card border border-border rounded-xl overflow-hidden shadow-xl">
|
||||||
<div class="px-5 py-3 border-b border-border">
|
<div class="px-5 py-3 border-b border-border">
|
||||||
<h2 class="text-sm font-semibold text-text-primary uppercase tracking-wider">
|
<h2 class="text-sm font-semibold text-text-primary uppercase tracking-wider">
|
||||||
📋 Upcoming Tasks
|
📋 Today's Routine
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-border">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-border">
|
||||||
{#each upcomingData.users as user}
|
{#each upcomingData.users as user}
|
||||||
|
{@const routineTasks = user.tasks.filter(t => t.recurrence > 0)}
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h3 class="text-xs font-bold text-accent mb-3 uppercase tracking-wider">{user.name}</h3>
|
<h3 class="text-xs font-bold text-accent mb-3 uppercase tracking-wider">{user.name}</h3>
|
||||||
{#if user.tasks.length === 0}
|
{#if routineTasks.length === 0}
|
||||||
<p class="text-[11px] text-text-secondary italic">No pending tasks</p>
|
<p class="text-[11px] text-text-secondary italic">No routine tasks</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2">
|
{#each [0, 1, 2] as periodIdx}
|
||||||
{#each user.tasks as task}
|
{@const tasksForPeriod = routineTasks.filter(t => t.period & (1 << periodIdx))}
|
||||||
<div class="flex items-start gap-2">
|
{#if tasksForPeriod.length > 0}
|
||||||
<span class="text-[10px] mt-0.5 {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono whitespace-nowrap">
|
<div class="mb-2">
|
||||||
|
<div class="text-[10px] uppercase tracking-wider text-text-secondary font-semibold mb-1">
|
||||||
|
{periodIcons[periodIdx]} {periodNames[periodIdx]}
|
||||||
|
</div>
|
||||||
|
{#each tasksForPeriod as task}
|
||||||
|
<div class="flex items-center gap-2 py-0.5 pl-3">
|
||||||
|
<span class="text-xs text-text-primary leading-tight">• {task.title}</span>
|
||||||
|
{#if task.recurrence > 0}
|
||||||
|
<span class="text-[9px] text-accent font-mono">
|
||||||
|
{task.recurrence === 0x7F ? '∞' : task.recurrence === 0x1F ? 'wk' : ''}
|
||||||
|
</span>
|
||||||
|
{:else if task.due_date > 0}
|
||||||
|
<span class="text-[9px] {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono">
|
||||||
{formatRelativeDate(task.due_date)}
|
{formatRelativeDate(task.due_date)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-text-primary leading-tight">{task.title}</span>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,17 +6,61 @@
|
|||||||
let tasks = $state([]);
|
let tasks = $state([]);
|
||||||
let error = $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
|
// Add task form state
|
||||||
let newTitle = $state('');
|
let newTitle = $state('');
|
||||||
|
let newPeriod = $state(0x01);
|
||||||
|
let newRecurrence = $state(0);
|
||||||
let newDueDay = $state('');
|
let newDueDay = $state('');
|
||||||
let newDueTime = $state('');
|
|
||||||
let showAddForm = $state(false);
|
let showAddForm = $state(false);
|
||||||
|
|
||||||
// Edit state
|
// Edit state
|
||||||
let editingTaskId = $state(null);
|
let editingTaskId = $state(null);
|
||||||
let editTitle = $state('');
|
let editTitle = $state('');
|
||||||
|
let editPeriod = $state(0);
|
||||||
|
let editRecurrence = $state(0);
|
||||||
let editDueDay = $state('');
|
let editDueDay = $state('');
|
||||||
let editDueTime = $state('');
|
|
||||||
|
|
||||||
// Confirm delete
|
// Confirm delete
|
||||||
let confirmDeleteId = $state(null);
|
let confirmDeleteId = $state(null);
|
||||||
@@ -28,8 +72,13 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
tasks = await getTasks(selectedUserId);
|
tasks = await getTasks(selectedUserId);
|
||||||
// Sort by due_date ascending
|
// Sort: recurrent first (by day), then one-off by due_date
|
||||||
tasks.sort((a, b) => a.due_date - b.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 = '';
|
error = '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.message;
|
error = e.message;
|
||||||
@@ -47,14 +96,20 @@
|
|||||||
|
|
||||||
async function handleAddTask(e) {
|
async function handleAddTask(e) {
|
||||||
e.preventDefault();
|
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 {
|
try {
|
||||||
await addTask(selectedUserId, newTitle.trim(), dueTimestamp);
|
await addTask(selectedUserId, newTitle.trim(), dueTimestamp, newPeriod, newRecurrence);
|
||||||
newTitle = '';
|
newTitle = '';
|
||||||
|
newPeriod = 0x01;
|
||||||
|
newRecurrence = 0;
|
||||||
newDueDay = '';
|
newDueDay = '';
|
||||||
newDueTime = '';
|
|
||||||
showAddForm = false;
|
showAddForm = false;
|
||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -74,20 +129,34 @@
|
|||||||
function startEditing(task) {
|
function startEditing(task) {
|
||||||
editingTaskId = task.id;
|
editingTaskId = task.id;
|
||||||
editTitle = task.title;
|
editTitle = task.title;
|
||||||
const parts = formatDateForInput(task.due_date);
|
editPeriod = task.period;
|
||||||
editDueDay = parts.day;
|
editRecurrence = task.recurrence;
|
||||||
editDueTime = parts.time;
|
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) {
|
async function saveEdit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!editTitle.trim()) return;
|
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 {
|
try {
|
||||||
await updateTask(editingTaskId, {
|
await updateTask(editingTaskId, {
|
||||||
title: editTitle.trim(),
|
title: editTitle.trim(),
|
||||||
due_date: dueTimestamp
|
due_date: dueTimestamp,
|
||||||
|
period: editPeriod,
|
||||||
|
recurrence: editRecurrence
|
||||||
});
|
});
|
||||||
editingTaskId = null;
|
editingTaskId = null;
|
||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
@@ -106,20 +175,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateForInput(timestamp) {
|
function setTodayForNewTask() {
|
||||||
const d = new Date(timestamp * 1000);
|
const d = new Date();
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
const dayNum = String(d.getDate()).padStart(2, '0');
|
const dayNum = String(d.getDate()).padStart(2, '0');
|
||||||
const hours = String(d.getHours()).padStart(2, '0');
|
newDueDay = `${year}-${month}-${dayNum}`;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeDate(timestamp) {
|
function formatRelativeDate(timestamp) {
|
||||||
@@ -139,8 +200,9 @@
|
|||||||
return diff < 0 ? `${days}d ago` : `in ${days}d`;
|
return diff < 0 ? `${days}d ago` : `in ${days}d`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOverdue(timestamp) {
|
function isOverdue(task) {
|
||||||
return timestamp < Date.now() / 1000;
|
if (task.recurrence > 0) return false; // Recurrent tasks don't have overdue
|
||||||
|
return task.due_date < Date.now() / 1000;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -151,7 +213,7 @@
|
|||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
<h2 class="task-title">Tasks</h2>
|
<h2 class="task-title">Tasks</h2>
|
||||||
{#if !showAddForm}
|
{#if !showAddForm}
|
||||||
<button class="add-task-btn" onclick={() => { showAddForm = true; setNowForNewTask(); }}>+ Add Task</button>
|
<button class="add-task-btn" onclick={() => { showAddForm = true; setTodayForNewTask(); }}>+ Add Task</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,39 +227,99 @@
|
|||||||
class="task-input"
|
class="task-input"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
<div class="date-time-row">
|
|
||||||
<label class="date-time-field">
|
<div class="field-row">
|
||||||
|
<div class="field-group">
|
||||||
|
<span class="field-label">Period</span>
|
||||||
|
<div class="period-selector">
|
||||||
|
{#each PERIODS as p, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="period-btn {hasBit(newPeriod, i) ? 'active' : ''}"
|
||||||
|
onclick={() => newPeriod = toggleBit(newPeriod, i)}
|
||||||
|
>{PERIOD_ICONS[i]} {p}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field-group">
|
||||||
|
<span class="field-label">Recurrence</span>
|
||||||
|
<div class="recurrence-selector">
|
||||||
|
{#each DAYS as day, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rec-btn {hasBit(newRecurrence, i) ? 'active' : ''}"
|
||||||
|
onclick={() => newRecurrence = toggleBit(newRecurrence, i)}
|
||||||
|
>{day}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if newRecurrence === 0}
|
||||||
|
<div class="field-row">
|
||||||
|
<label class="field-group">
|
||||||
<span class="field-label">Date</span>
|
<span class="field-label">Date</span>
|
||||||
<input type="date" bind:value={newDueDay} class="task-date-input" />
|
<input type="date" bind:value={newDueDay} class="task-date-input" />
|
||||||
</label>
|
</label>
|
||||||
<label class="date-time-field">
|
|
||||||
<span class="field-label">Time</span>
|
|
||||||
<input type="time" bind:value={newDueTime} class="task-time-input" />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDay || !newDueTime}>Add</button>
|
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || (newRecurrence === 0 && !newDueDay)}>Add</button>
|
||||||
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newDueTime = ''; }}>Cancel</button>
|
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newRecurrence = 0; newPeriod = 0x01; }}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="task-list">
|
<div class="task-list">
|
||||||
{#each tasks as task}
|
{#each tasks as task}
|
||||||
<div class="task-item {task.completed ? 'completed' : ''} {isOverdue(task.due_date) && !task.completed ? 'overdue' : ''}">
|
<div class="task-item {task.completed ? 'completed' : ''} {isOverdue(task) && !task.completed ? 'overdue' : ''}">
|
||||||
{#if editingTaskId === task.id}
|
{#if editingTaskId === task.id}
|
||||||
<form class="edit-form" onsubmit={saveEdit}>
|
<form class="edit-form" onsubmit={saveEdit}>
|
||||||
<input type="text" bind:value={editTitle} class="task-input" />
|
<input type="text" bind:value={editTitle} class="task-input" />
|
||||||
<div class="date-time-row">
|
|
||||||
<label class="date-time-field">
|
<div class="field-row">
|
||||||
|
<div class="field-group">
|
||||||
|
<span class="field-label">Period</span>
|
||||||
|
<div class="period-selector">
|
||||||
|
{#each PERIODS as p, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="period-btn {hasBit(editPeriod, i) ? 'active' : ''}"
|
||||||
|
onclick={() => editPeriod = toggleBit(editPeriod, i)}
|
||||||
|
>{PERIOD_ICONS[i]} {p}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="field-group">
|
||||||
|
<span class="field-label">Recurrence</span>
|
||||||
|
<div class="recurrence-selector">
|
||||||
|
{#each DAYS as day, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rec-btn {hasBit(editRecurrence, i) ? 'active' : ''}"
|
||||||
|
onclick={() => editRecurrence = toggleBit(editRecurrence, i)}
|
||||||
|
>{day}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if editRecurrence === 0}
|
||||||
|
<div class="field-row">
|
||||||
|
<label class="field-group">
|
||||||
<span class="field-label">Date</span>
|
<span class="field-label">Date</span>
|
||||||
<input type="date" bind:value={editDueDay} class="task-date-input" />
|
<input type="date" bind:value={editDueDay} class="task-date-input" />
|
||||||
</label>
|
</label>
|
||||||
<label class="date-time-field">
|
|
||||||
<span class="field-label">Time</span>
|
|
||||||
<input type="time" bind:value={editDueTime} class="task-time-input" />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn-primary btn-sm">Save</button>
|
<button type="submit" class="btn-primary btn-sm">Save</button>
|
||||||
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
|
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
|
||||||
@@ -213,9 +335,16 @@
|
|||||||
/>
|
/>
|
||||||
<div class="task-info">
|
<div class="task-info">
|
||||||
<span class="task-text">{task.title}</span>
|
<span class="task-text">{task.title}</span>
|
||||||
<span class="task-due {isOverdue(task.due_date) && !task.completed ? 'text-overdue' : ''}">
|
<div class="task-meta">
|
||||||
|
<span class="task-period">{periodIcons(task.period)} {formatPeriod(task.period)}</span>
|
||||||
|
{#if task.recurrence > 0}
|
||||||
|
<span class="task-recurrence">🔁 {formatRecurrence(task.recurrence)}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="task-due {isOverdue(task) && !task.completed ? 'text-overdue' : ''}">
|
||||||
{formatRelativeDate(task.due_date)}
|
{formatRelativeDate(task.due_date)}
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-actions">
|
<div class="task-actions">
|
||||||
@@ -305,18 +434,15 @@
|
|||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-time-row {
|
.field-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-time-field {
|
.field-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.date-time-field:first-child {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +454,65 @@
|
|||||||
color: var(--color-text-secondary);
|
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 {
|
.task-date-input {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -342,23 +527,8 @@
|
|||||||
position: relative;
|
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) */
|
/* Make the native picker icon cover the entire input (Chrome/Edge only) */
|
||||||
.task-date-input::-webkit-calendar-picker-indicator,
|
.task-date-input::-webkit-calendar-picker-indicator {
|
||||||
.task-time-input::-webkit-calendar-picker-indicator {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -370,7 +540,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-date-input:focus, .task-time-input:focus {
|
.task-date-input:focus {
|
||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +600,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -437,6 +608,11 @@
|
|||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-item .edit-form {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.task-item:hover {
|
.task-item:hover {
|
||||||
background: var(--color-bg-card-hover);
|
background: var(--color-bg-card-hover);
|
||||||
}
|
}
|
||||||
@@ -485,6 +661,25 @@
|
|||||||
color: var(--color-text-secondary);
|
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 {
|
.task-due {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
@@ -222,14 +222,16 @@ export async function getUpcomingTasks() {
|
|||||||
* Create a new task.
|
* Create a new task.
|
||||||
* @param {number} userId
|
* @param {number} userId
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @param {number} dueDate Unix timestamp in seconds
|
* @param {number} dueDate Unix timestamp in seconds (used for non-recurrent tasks)
|
||||||
* @returns {Promise<{id: number, user_id: number, title: string, due_date: number, completed: boolean}>}
|
* @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`, {
|
const res = await trackedFetch(`${API_BASE}/api/tasks`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (!res.ok) {
|
||||||
const errorText = await res.text();
|
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).
|
* Update a task (partial update — only include fields you want to change).
|
||||||
* @param {number} id
|
* @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}>}
|
* @returns {Promise<{status: string}>}
|
||||||
*/
|
*/
|
||||||
export async function updateTask(id, fields) {
|
export async function updateTask(id, fields) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"major": 0,
|
"major": 0,
|
||||||
"minor": 1,
|
"minor": 1,
|
||||||
"revision": 23
|
"revision": 30
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// POST /api/tasks — Create a new task
|
// 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 "cJSON.h"
|
||||||
#include "esp_http_server.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 *user_id_item = cJSON_GetObjectItem(body, "user_id");
|
||||||
cJSON *title_item = cJSON_GetObjectItem(body, "title");
|
cJSON *title_item = cJSON_GetObjectItem(body, "title");
|
||||||
cJSON *due_date_item = cJSON_GetObjectItem(body, "due_date");
|
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) ||
|
if (!cJSON_IsNumber(user_id_item) || !cJSON_IsString(title_item))
|
||||||
!cJSON_IsNumber(due_date_item))
|
|
||||||
{
|
{
|
||||||
cJSON_Delete(body);
|
cJSON_Delete(body);
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing user_id or title");
|
||||||
"Missing user_id, title, or due_date");
|
|
||||||
return ESP_FAIL;
|
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 =
|
task_t *task =
|
||||||
add_task((uint8)user_id_item->valueint, title_item->valuestring,
|
add_task((uint8)user_id_item->valueint, title_item->valuestring, due_date,
|
||||||
(int64)due_date_item->valuedouble);
|
period, recurrence);
|
||||||
cJSON_Delete(body);
|
cJSON_Delete(body);
|
||||||
|
|
||||||
if (!task)
|
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_AddNumberToObject(resp, "user_id", task->user_id);
|
||||||
cJSON_AddStringToObject(resp, "title", task->title);
|
cJSON_AddStringToObject(resp, "title", task->title);
|
||||||
cJSON_AddNumberToObject(resp, "due_date", (double)task->due_date);
|
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);
|
cJSON_AddBoolToObject(resp, "completed", task->completed);
|
||||||
|
|
||||||
const char *json = cJSON_PrintUnformatted(resp);
|
const char *json = cJSON_PrintUnformatted(resp);
|
||||||
|
|||||||
@@ -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_AddNumberToObject(obj, "user_id", g_Tasks[i].user_id);
|
||||||
cJSON_AddStringToObject(obj, "title", g_Tasks[i].title);
|
cJSON_AddStringToObject(obj, "title", g_Tasks[i].title);
|
||||||
cJSON_AddNumberToObject(obj, "due_date", (double)g_Tasks[i].due_date);
|
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_AddBoolToObject(obj, "completed", g_Tasks[i].completed);
|
||||||
cJSON_AddItemToArray(arr, obj);
|
cJSON_AddItemToArray(arr, obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ task_t *find_task(uint16 id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a task, returns pointer to new task or nullptr if full
|
// 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
|
// Verify user exists
|
||||||
if (find_user(user_id) == nullptr)
|
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;
|
g_Tasks[i].user_id = user_id;
|
||||||
strlcpy(g_Tasks[i].title, title, sizeof(g_Tasks[i].title));
|
strlcpy(g_Tasks[i].title, title, sizeof(g_Tasks[i].title));
|
||||||
g_Tasks[i].due_date = due_date;
|
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].completed = false;
|
||||||
g_Tasks[i].active = true;
|
g_Tasks[i].active = true;
|
||||||
return &g_Tasks[i];
|
return &g_Tasks[i];
|
||||||
@@ -94,18 +97,19 @@ void seed_tasks()
|
|||||||
{
|
{
|
||||||
int64 now = (int64)(esp_timer_get_time() / 1000000);
|
int64 now = (int64)(esp_timer_get_time() / 1000000);
|
||||||
|
|
||||||
// Alice's tasks (user_id = 1)
|
// Alice's tasks (user_id = 1) — mix of one-off and recurring
|
||||||
add_task(1, "Buy groceries", now + 86400); // +1 day
|
add_task(1, "Buy groceries", now + 86400, PERIOD_MORNING);
|
||||||
add_task(1, "Review PR #42", now + 3600); // +1 hour
|
add_task(1, "Review PR #42", now + 3600, PERIOD_AFTERNOON);
|
||||||
add_task(1, "Book dentist appointment", now + 172800); // +2 days
|
add_task(1, "Book dentist appointment", now + 172800, PERIOD_MORNING);
|
||||||
add_task(1, "Update resume", now + 604800); // +7 days
|
add_task(1, "Update resume", now + 604800, PERIOD_EVENING);
|
||||||
|
|
||||||
// Bob's tasks (user_id = 2)
|
// Bob's tasks (user_id = 2) — some recurring routines
|
||||||
add_task(2, "Fix login bug", now + 7200); // +2 hours
|
add_task(2, "Morning standup", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
|
||||||
add_task(2, "Deploy staging", now + 43200); // +12 hours
|
add_task(2, "Deploy staging", now + 43200, PERIOD_AFTERNOON);
|
||||||
add_task(2, "Write unit tests", now + 259200); // +3 days
|
add_task(2, "Write unit tests", now + 259200, PERIOD_MORNING);
|
||||||
|
|
||||||
// Charlie's tasks (user_id = 3)
|
// Charlie's tasks (user_id = 3) — kid routine examples
|
||||||
add_task(3, "Water plants", now + 1800); // +30 min
|
add_task(3, "Breakfast", 0, PERIOD_MORNING, 0x1F); // Mon-Fri
|
||||||
add_task(3, "Call plumber", now + 86400); // +1 day
|
add_task(3, "Homework", 0, PERIOD_AFTERNOON, 0x15); // Mon+Wed+Fri
|
||||||
|
add_task(3, "Bath time", 0, PERIOD_EVENING, 0x7F); // Every day
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
// Data store operations for tasks
|
// Data store operations for tasks
|
||||||
task_t *find_task(uint16 id);
|
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);
|
bool remove_task(uint16 id);
|
||||||
void remove_tasks_for_user(uint8 user_id);
|
void remove_tasks_for_user(uint8 user_id);
|
||||||
void sort_tasks_by_due_date(task_t **arr, int count);
|
void sort_tasks_by_due_date(task_t **arr, int count);
|
||||||
|
|||||||
@@ -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 "cJSON.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
@@ -20,36 +20,26 @@ internal esp_err_t api_tasks_upcoming_handler(httpd_req_t *req)
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Collect incomplete tasks for this user
|
// Collect incomplete tasks for this user
|
||||||
task_t *user_tasks[MAX_TASKS];
|
// Include: recurring tasks (any day) and one-off 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
|
|
||||||
cJSON *user_obj = cJSON_CreateObject();
|
cJSON *user_obj = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(user_obj, "id", g_Users[u].id);
|
cJSON_AddNumberToObject(user_obj, "id", g_Users[u].id);
|
||||||
cJSON_AddStringToObject(user_obj, "name", g_Users[u].name);
|
cJSON_AddStringToObject(user_obj, "name", g_Users[u].name);
|
||||||
|
|
||||||
cJSON *tasks_arr = cJSON_AddArrayToObject(user_obj, "tasks");
|
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 *t_obj = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(t_obj, "id", user_tasks[i]->id);
|
cJSON_AddNumberToObject(t_obj, "id", g_Tasks[t].id);
|
||||||
cJSON_AddStringToObject(t_obj, "title", user_tasks[i]->title);
|
cJSON_AddStringToObject(t_obj, "title", g_Tasks[t].title);
|
||||||
cJSON_AddNumberToObject(t_obj, "due_date",
|
cJSON_AddNumberToObject(t_obj, "due_date", (double)g_Tasks[t].due_date);
|
||||||
(double)user_tasks[i]->due_date);
|
cJSON_AddNumberToObject(t_obj, "period", g_Tasks[t].period);
|
||||||
cJSON_AddBoolToObject(t_obj, "completed", user_tasks[i]->completed);
|
cJSON_AddNumberToObject(t_obj, "recurrence", g_Tasks[t].recurrence);
|
||||||
|
cJSON_AddBoolToObject(t_obj, "completed", g_Tasks[t].completed);
|
||||||
cJSON_AddItemToArray(tasks_arr, t_obj);
|
cJSON_AddItemToArray(tasks_arr, t_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// POST /api/tasks/update — Modify a task
|
// POST /api/tasks/update — Modify a task
|
||||||
// Body: {"id":1, "title":"...", "due_date":..., "completed":true}
|
// Body: {"id":1, "title":"...", "due_date":..., "period":0, "recurrence":0,
|
||||||
// All fields except "id" are optional
|
// "completed":true} All fields except "id" are optional
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "esp_http_server.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;
|
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");
|
cJSON *completed_item = cJSON_GetObjectItem(body, "completed");
|
||||||
if (cJSON_IsBool(completed_item))
|
if (cJSON_IsBool(completed_item))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,14 +5,24 @@
|
|||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
#include "user.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
|
struct task_t
|
||||||
{
|
{
|
||||||
|
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)
|
uint16 id; // Auto-assigned (1–65535, 0 = empty slot)
|
||||||
uint8 user_id; // Owner (matches user_t.id)
|
uint8 user_id; // Owner (matches user_t.id)
|
||||||
char title[64]; // Task description
|
uint8 recurrence; // Bitmask: bit0=Mon, bit1=Tue, ..., bit6=Sun. 0=none
|
||||||
int64 due_date; // Unix timestamp (seconds)
|
uint8 period : 3; // Bitmask: bit0=Morning, bit1=Afternoon, bit2=Evening
|
||||||
bool completed; // Done flag
|
bool completed : 1; // Done flag
|
||||||
bool active; // Slot in use
|
bool active : 1; // Slot in use
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int MAX_TASKS = 32;
|
constexpr int MAX_TASKS = 32;
|
||||||
|
|||||||
Reference in New Issue
Block a user