Feature chore bug 007 #21
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user