Add strike events to activity log

Record a StrikeEvent row whenever a strike is added or removed,
and surface them in the activity log timeline with timestamp,
child name, and whether the strike was added or removed.

- New strike_events table (auto-created on startup)
- children router records prev/new strikes on every update
- GET /api/logs/strikes and DELETE /api/logs/strikes/:id endpoints
- Log view merges strike entries into the timeline (red dot,
  "✕ Strike added (2/3)" / "↩ Strike removed (1/3)")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 22:07:41 -08:00
parent f730e9edf9
commit b5f4188396
6 changed files with 127 additions and 9 deletions

View File

@@ -85,7 +85,7 @@
<div
class="event-dot"
:style="entry.subject_color ? { background: entry.subject_color } : {}"
:class="{ 'dot-note': entry._type === 'note' }"
:class="{ 'dot-note': entry._type === 'note', 'dot-strike': entry._type === 'strike' }"
></div>
<div class="event-body">
<div class="event-label">
@@ -101,7 +101,8 @@
</div>
</div>
<div class="row-actions">
<button class="btn-sm" v-if="entry.event_type !== 'session_start'" @click="startEdit(entry)">Edit</button>
<button class="btn-sm" v-if="entry._type === 'event' && entry.event_type !== 'session_start'" @click="startEdit(entry)">Edit</button>
<button class="btn-sm" v-if="entry._type === 'note'" @click="startEdit(entry)">Edit</button>
<button class="btn-sm btn-danger" @click="deleteEntry(entry)"></button>
</div>
</div>
@@ -129,6 +130,7 @@ const childrenStore = useChildrenStore()
const authStore = useAuthStore()
const timeline = ref([])
const manualLogs = ref([])
const strikeEvents = ref([])
const showForm = ref(false)
const filterDate = ref(new Date().toISOString().split('T')[0])
@@ -140,7 +142,7 @@ const activeChildId = computed(() => childrenStore.activeChild?.id || null)
const editingEntry = ref(null)
const editDraft = ref({})
// Merge and sort timeline events + manual notes
// Merge and sort timeline events + manual notes + strike events
const combinedEntries = computed(() => {
const events = timeline.value.map(e => ({
...e,
@@ -159,10 +161,18 @@ const combinedEntries = computed(() => {
subject_icon: null,
subject_color: null,
}))
return [...events, ...notes].sort((a, b) => b.occurred_at.localeCompare(a.occurred_at))
const strikes = strikeEvents.value.map(s => ({
...s,
_type: 'strike',
_key: `strike-${s.id}`,
subject_name: null,
subject_icon: null,
subject_color: null,
}))
return [...events, ...notes, ...strikes].sort((a, b) => b.occurred_at.localeCompare(a.occurred_at))
})
// Group by session_date (events) or log_date (notes)
// Group by session_date (events) or log_date (notes/strikes)
const groupedByDate = computed(() => {
const groups = {}
for (const entry of combinedEntries.value) {
@@ -193,12 +203,19 @@ const EVENT_META = {
function eventIcon(entry) {
if (entry._type === 'note') return '📝'
if (entry._type === 'strike') return entry.new_strikes > entry.prev_strikes ? '✕' : '↩'
if (entry.event_type === 'complete' && !entry.block_label) return '🏁'
return EVENT_META[entry.event_type]?.icon || '•'
}
function eventLabel(entry) {
if (entry._type === 'note') return 'Note'
if (entry._type === 'strike') {
const added = entry.new_strikes > entry.prev_strikes
return added
? `Strike added (${entry.new_strikes}/3)`
: `Strike removed (${entry.new_strikes}/3)`
}
if (entry.event_type === 'complete' && !entry.block_label) return 'Day ended'
const action = EVENT_META[entry.event_type]?.label || entry.event_type
return entry.block_label ? `${action}${entry.block_label}` : action
@@ -236,6 +253,9 @@ async function deleteEntry(entry) {
if (entry._type === 'event') {
await api.delete(`/api/logs/timeline/${entry.id}`)
timeline.value = timeline.value.filter(e => e.id !== entry.id)
} else if (entry._type === 'strike') {
await api.delete(`/api/logs/strikes/${entry.id}`)
strikeEvents.value = strikeEvents.value.filter(s => s.id !== entry.id)
} else {
await api.delete(`/api/logs/${entry.id}`)
manualLogs.value = manualLogs.value.filter(l => l.id !== entry.id)
@@ -247,12 +267,14 @@ async function loadData() {
if (activeChildId.value) params.child_id = activeChildId.value
if (filterDate.value) params.log_date = filterDate.value
const [tRes, lRes] = await Promise.all([
const [tRes, lRes, sRes] = await Promise.all([
api.get('/api/logs/timeline', { params }),
api.get('/api/logs', { params }),
api.get('/api/logs/strikes', { params }),
])
timeline.value = tRes.data
manualLogs.value = lRes.data
strikeEvents.value = sRes.data
}
async function createLog() {
@@ -367,6 +389,7 @@ h1 { font-size: 1.75rem; font-weight: 700; }
}
.event-dot.dot-note { background: #4f46e5; }
.event-dot.dot-strike { background: #ef4444; }
.event-body { flex: 1; min-width: 0; }