Feature chore bug 007 #21

Merged
iamdoubz merged 68 commits from feature_chore_bug_007 into main 2026-07-12 12:59:04 -05:00
Showing only changes of commit 836547596b - Show all commits
+133 -38
View File
@@ -30,6 +30,8 @@
Check,
Cpu,
Globe,
Plus,
X,
} from "@lucide/svelte";
onMount(() => calendar.load());
@@ -143,20 +145,47 @@
}
}
// Editable copy so toggling a checkbox doesn't persist until "Save" is
// pressed; recomputes whenever a different meeting (or a freshly generated
// summary) comes in. bind:checked mutates each item in place, which a plain
// $derived array tolerates fine — only the array identity is recomputed.
let editableItems = $derived(
(meetings.selected?.summary?.action_items ?? []).map((i) => ({ ...i }) as ActionItem),
);
// Editable working copy so add/delete/edit don't persist until "Save" is
// pressed. Resynced from the meeting's table-backed action_items whenever a
// *different* meeting is selected (same guard pattern as pendingTags), so
// it isn't clobbered mid-edit.
let editableItems = $state<ActionItem[]>([]);
let loadedItemsForId: string | null = null;
$effect(() => {
const m = meetings.selected;
if (m && m.id !== loadedItemsForId) {
editableItems = m.action_items.map((i) => ({ ...i }));
loadedItemsForId = m.id;
} else if (!m) {
loadedItemsForId = null;
}
});
function addActionItem() {
editableItems = [
...editableItems,
{ id: null, text: "", owner: null, due_at: null, confirmed: false, reminder_set: false },
];
}
function removeActionItem(i: number) {
editableItems = editableItems.filter((_, idx) => idx !== i);
}
let savingItems = $state(false);
async function saveActionItems() {
const m = meetings.selected;
if (!m) return;
savingItems = true;
try {
await meetings.confirmActionItems(m.id, editableItems);
// Drop blank rows (an added-but-never-typed item) rather than persisting
// empty tasks; keep null owner rather than "".
const items = editableItems
.filter((i) => i.text.trim())
.map((i) => ({ ...i, owner: i.owner?.trim() || null }));
await meetings.confirmActionItems(m.id, items);
// confirmActionItems reloaded the meeting — resync so freshly inserted
// rows carry their new ids (a second save would otherwise re-insert them).
editableItems = (meetings.selected?.action_items ?? []).map((i) => ({ ...i }));
} finally {
savingItems = false;
}
@@ -709,35 +738,66 @@
<h3><ListTodo size={14} aria-hidden="true" /> Action items</h3>
{#if !meetings.selected}
<p class="muted">Select a meeting to see its action items.</p>
{:else if editableItems.length === 0}
<p class="muted">Parsed from the summary; edit and confirm before they become tasks.</p>
{:else}
<ul class="action-items">
{#each editableItems as item, i (i)}
<li>
<label>
<input type="checkbox" bind:checked={item.confirmed} />
<span>{item.text}</span>
</label>
{#if item.owner}<span class="muted small">{item.owner}</span>{/if}
<input
type="date"
class="due-date"
aria-label="Due date"
value={dueDateInput(item.due_at)}
onchange={(e) => onDueDateChange(item, (e.target as HTMLInputElement).value)}
/>
<label class="remind" title="Schedule a local reminder for this due date">
<input type="checkbox" bind:checked={item.reminder_set} disabled={!item.due_at} />
<Bell size={14} aria-hidden="true" />
</label>
</li>
{/each}
</ul>
<button class="primary" onclick={saveActionItems} disabled={savingItems}>
<Check size={14} aria-hidden="true" />
{savingItems ? "Saving…" : "Save action items"}
</button>
{#if editableItems.length === 0}
<p class="muted">
None yet — add one below, or generate a summary to extract them automatically.
</p>
{:else}
<ul class="action-items">
{#each editableItems as item, i (i)}
<li>
<input
type="checkbox"
bind:checked={item.confirmed}
aria-label="Confirmed"
title="Confirmed"
/>
<input
class="ai-text"
bind:value={item.text}
placeholder="Action item…"
aria-label="Action item text"
/>
<input
class="ai-owner"
value={item.owner ?? ""}
oninput={(e) => (item.owner = (e.target as HTMLInputElement).value || null)}
placeholder="Owner"
aria-label="Owner"
/>
<input
type="date"
class="due-date"
aria-label="Due date"
value={dueDateInput(item.due_at)}
onchange={(e) => onDueDateChange(item, (e.target as HTMLInputElement).value)}
/>
<label class="remind" title="Schedule a local reminder for this due date">
<input type="checkbox" bind:checked={item.reminder_set} disabled={!item.due_at} />
<Bell size={14} aria-hidden="true" />
</label>
<button
class="ai-del"
onclick={() => removeActionItem(i)}
title="Delete this action item"
aria-label="Delete action item"
>
<X size={14} aria-hidden="true" />
</button>
</li>
{/each}
</ul>
{/if}
<div class="ai-actions">
<button class="link" onclick={addActionItem}>
<Plus size={14} aria-hidden="true" /> Add action item
</button>
<button class="primary" onclick={saveActionItems} disabled={savingItems}>
<Check size={14} aria-hidden="true" />
{savingItems ? "Saving…" : "Save action items"}
</button>
</div>
{/if}
<h3><CalendarDays size={14} aria-hidden="true" /> Calendar event</h3>
@@ -982,8 +1042,7 @@
ul.action-items li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
gap: 0.35rem;
padding: 0.25rem 0;
border-bottom: 1px solid var(--border);
}
@@ -992,6 +1051,42 @@
align-items: center;
gap: 0.4rem;
}
.ai-text {
flex: 1;
min-width: 0;
font-size: 0.82rem;
}
.ai-owner {
flex: none;
width: 5rem;
font-size: 0.75rem;
}
.ai-del {
display: flex;
align-items: center;
flex: none;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
padding: 0.15rem;
border-radius: var(--radius-sm);
}
.ai-del:hover {
color: var(--danger);
background: var(--bg-hover);
}
.ai-actions {
display: flex;
align-items: center;
gap: 0.8rem;
margin-top: 0.5rem;
}
.ai-actions .link {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.due-date {
font-size: 0.75rem;
padding: 0.2rem;