Updated the task view and backend to handle more like a routine
This commit is contained in:
@@ -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;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -151,7 +213,7 @@
|
||||
<div class="task-header">
|
||||
<h2 class="task-title">Tasks</h2>
|
||||
{#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}
|
||||
</div>
|
||||
|
||||
@@ -165,39 +227,99 @@
|
||||
class="task-input"
|
||||
autofocus
|
||||
/>
|
||||
<div class="date-time-row">
|
||||
<label class="date-time-field">
|
||||
<span class="field-label">Date</span>
|
||||
<input type="date" bind:value={newDueDay} class="task-date-input" />
|
||||
</label>
|
||||
<label class="date-time-field">
|
||||
<span class="field-label">Time</span>
|
||||
<input type="time" bind:value={newDueTime} class="task-time-input" />
|
||||
</label>
|
||||
|
||||
<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>
|
||||
<input type="date" bind:value={newDueDay} class="task-date-input" />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDay || !newDueTime}>Add</button>
|
||||
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newDueTime = ''; }}>Cancel</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 = ''; newRecurrence = 0; newPeriod = 0x01; }}>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<div class="task-list">
|
||||
{#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}
|
||||
<form class="edit-form" onsubmit={saveEdit}>
|
||||
<input type="text" bind:value={editTitle} class="task-input" />
|
||||
<div class="date-time-row">
|
||||
<label class="date-time-field">
|
||||
<span class="field-label">Date</span>
|
||||
<input type="date" bind:value={editDueDay} class="task-date-input" />
|
||||
</label>
|
||||
<label class="date-time-field">
|
||||
<span class="field-label">Time</span>
|
||||
<input type="time" bind:value={editDueTime} class="task-time-input" />
|
||||
</label>
|
||||
|
||||
<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>
|
||||
<input type="date" bind:value={editDueDay} class="task-date-input" />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-primary btn-sm">Save</button>
|
||||
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
|
||||
@@ -213,9 +335,16 @@
|
||||
/>
|
||||
<div class="task-info">
|
||||
<span class="task-text">{task.title}</span>
|
||||
<span class="task-due {isOverdue(task.due_date) && !task.completed ? 'text-overdue' : ''}">
|
||||
{formatRelativeDate(task.due_date)}
|
||||
</span>
|
||||
<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)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user