Enables marking scheduled events as complete

Implements a feature to mark scheduled events as complete, replacing the checkbox with a button for improved UX.

This commit enhances the Scheduled Events functionality by allowing users to mark events as complete. It also includes:

- Replaces the checkbox with a "Mark Complete" button for better usability.
- Adds logging for debugging purposes during event completion toggling.
- Routes completion events (kind 31925) to the ScheduledEventService.
- Optimistically updates the local state after publishing completion events.
This commit is contained in:
padreug 2025-10-21 22:31:35 +02:00
parent 9b05bcc238
commit 4050b33d0e
5 changed files with 91 additions and 60 deletions

View file

@ -169,10 +169,12 @@ async function onToggleLike(note: FeedPost) {
// Handle scheduled event completion toggle
async function onToggleComplete(event: ScheduledEvent) {
console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title)
try {
await toggleComplete(event)
console.log('✅ NostrFeed: toggleComplete succeeded')
} catch (error) {
console.error('Failed to toggle event completion:', error)
console.error('❌ NostrFeed: Failed to toggle event completion:', error)
}
}
@ -397,8 +399,8 @@ function cancelDelete() {
v-for="event in todaysScheduledEvents"
:key="`${event.pubkey}:${event.dTag}`"
:event="event"
:completion="getCompletion(`31922:${event.pubkey}:${event.dTag}`)"
:get-display-name="getDisplayName"
:get-completion="getCompletion"
:admin-pubkeys="adminPubkeys"
@toggle-complete="onToggleComplete"
/>

View file

@ -1,14 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Calendar, MapPin, Clock } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next'
import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
interface Props {
event: ScheduledEvent
completion?: EventCompletion
getDisplayName: (pubkey: string) => string
getCompletion: (eventAddress: string) => EventCompletion | undefined
adminPubkeys?: string[]
}
@ -22,11 +22,14 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>()
// Event address for tracking completion
const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`)
// Check if this is an admin event
const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey))
// Check if event is completed
const isCompleted = computed(() => props.completion?.completed || false)
// Check if event is completed - call function directly
const isCompleted = computed(() => props.getCompletion(eventAddress.value)?.completed || false)
// Format the date/time
const formattedDate = computed(() => {
@ -79,8 +82,9 @@ const formattedTimeRange = computed(() => {
}
})
// Handle checkbox toggle
function handleToggleComplete() {
// Handle mark complete button click
function handleMarkComplete() {
console.log('🔘 Mark Complete button clicked for event:', props.event.title)
emit('toggle-complete', props.event)
}
</script>
@ -88,63 +92,64 @@ function handleToggleComplete() {
<template>
<div class="border-b md:border md:rounded-lg bg-card p-4 md:p-6 transition-all"
:class="{ 'opacity-60': isCompleted }">
<!-- Header -->
<div class="flex items-start gap-3 mb-3">
<!-- Completion Checkbox -->
<div class="flex items-center pt-1">
<Checkbox
:checked="isCompleted"
@update:checked="handleToggleComplete"
class="h-5 w-5"
/>
<!-- Event Details -->
<div class="flex-1 min-w-0">
<!-- Title and badges -->
<div class="flex items-start gap-2 mb-2 flex-wrap">
<h3 class="font-semibold text-base md:text-lg flex-1"
:class="{ 'line-through': isCompleted }">
{{ event.title }}
</h3>
<Badge v-if="isAdminEvent" variant="secondary" class="shrink-0">
Admin
</Badge>
</div>
<!-- Event Details -->
<div class="flex-1 min-w-0">
<!-- Title and badges -->
<div class="flex items-start gap-2 mb-2 flex-wrap">
<h3 class="font-semibold text-base md:text-lg flex-1"
:class="{ 'line-through': isCompleted }">
{{ event.title }}
</h3>
<Badge v-if="isAdminEvent" variant="secondary" class="shrink-0">
Admin
</Badge>
<!-- Date/Time -->
<div class="flex items-center gap-4 text-sm text-muted-foreground mb-2 flex-wrap">
<div class="flex items-center gap-1.5">
<Calendar class="h-4 w-4" />
<span>{{ formattedDate }}</span>
</div>
<div v-if="formattedTimeRange" class="flex items-center gap-1.5">
<Clock class="h-4 w-4" />
<span>{{ formattedTimeRange }}</span>
</div>
</div>
<!-- Date/Time -->
<div class="flex items-center gap-4 text-sm text-muted-foreground mb-2 flex-wrap">
<div class="flex items-center gap-1.5">
<Calendar class="h-4 w-4" />
<span>{{ formattedDate }}</span>
</div>
<div v-if="formattedTimeRange" class="flex items-center gap-1.5">
<Clock class="h-4 w-4" />
<span>{{ formattedTimeRange }}</span>
</div>
</div>
<!-- Location -->
<div v-if="event.location" class="flex items-center gap-1.5 text-sm text-muted-foreground mb-3">
<MapPin class="h-4 w-4" />
<span>{{ event.location }}</span>
</div>
<!-- Location -->
<div v-if="event.location" class="flex items-center gap-1.5 text-sm text-muted-foreground mb-3">
<MapPin class="h-4 w-4" />
<span>{{ event.location }}</span>
</div>
<!-- Description/Content -->
<div v-if="event.description || event.content" class="text-sm mb-3">
<p class="whitespace-pre-wrap break-words">{{ event.description || event.content }}</p>
</div>
<!-- Description/Content -->
<div v-if="event.description || event.content" class="text-sm mb-3">
<p class="whitespace-pre-wrap break-words">{{ event.description || event.content }}</p>
</div>
<!-- Completion info -->
<div v-if="isCompleted && getCompletion(eventAddress)" class="text-xs text-muted-foreground mb-3">
Completed by {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }}
<span v-if="getCompletion(eventAddress)!.notes"> - {{ getCompletion(eventAddress)!.notes }}</span>
</div>
<!-- Completion info -->
<div v-if="isCompleted && completion" class="text-xs text-muted-foreground">
Completed by {{ getDisplayName(completion.pubkey) }}
<span v-if="completion.notes"> - {{ completion.notes }}</span>
</div>
<!-- Author (if not admin) -->
<div v-if="!isAdminEvent" class="text-xs text-muted-foreground mb-3">
Posted by {{ getDisplayName(event.pubkey) }}
</div>
<!-- Author (if not admin) -->
<div v-if="!isAdminEvent" class="text-xs text-muted-foreground mt-2">
Posted by {{ getDisplayName(event.pubkey) }}
</div>
<!-- Mark Complete Button -->
<div v-if="!isCompleted" class="mt-3">
<Button
@click="handleMarkComplete"
variant="outline"
size="sm"
class="gap-2"
>
<CheckCircle class="h-4 w-4" />
Mark Complete
</Button>
</div>
</div>
</div>

View file

@ -54,7 +54,10 @@ export function useScheduledEvents() {
* Toggle completion status of an event
*/
const toggleComplete = async (event: ScheduledEvent, notes: string = ''): Promise<void> => {
console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title)
if (!scheduledEventService) {
console.error('❌ useScheduledEvents: Scheduled event service not available')
toast.error('Scheduled event service not available')
return
}
@ -62,11 +65,14 @@ export function useScheduledEvents() {
try {
const eventAddress = `31922:${event.pubkey}:${event.dTag}`
const currentlyCompleted = scheduledEventService.isCompleted(eventAddress)
console.log('📊 useScheduledEvents: Current completion status:', currentlyCompleted)
if (currentlyCompleted) {
console.log('⬇️ useScheduledEvents: Marking as incomplete...')
await scheduledEventService.uncompleteEvent(event)
toast.success('Event marked as incomplete')
} else {
console.log('⬆️ useScheduledEvents: Marking as complete...')
await scheduledEventService.completeEvent(event, notes)
toast.success('Event completed!')
}
@ -81,7 +87,7 @@ export function useScheduledEvents() {
toast.error(message)
}
console.error('Failed to toggle completion:', error)
console.error('❌ useScheduledEvents: Failed to toggle completion:', error)
}
}

View file

@ -276,8 +276,11 @@ export class FeedService extends BaseService {
// Route RSVP/completion events (kind 31925) to ScheduledEventService
if (event.kind === 31925) {
console.log('🔀 FeedService: Routing kind 31925 (completion) to ScheduledEventService')
if (this.scheduledEventService) {
this.scheduledEventService.handleCompletionEvent(event)
} else {
console.warn('⚠️ FeedService: ScheduledEventService not available')
}
return
}

View file

@ -113,6 +113,8 @@ export class ScheduledEventService extends BaseService {
* Made public so FeedService can route kind 31925 events to this service
*/
public handleCompletionEvent(event: NostrEvent): void {
console.log('🔔 ScheduledEventService: Received completion event (kind 31925)', event.id)
try {
// Find the event being responded to
const aTag = event.tags.find(tag => tag[0] === 'a')?.[1]
@ -125,6 +127,13 @@ export class ScheduledEventService extends BaseService {
const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1]
const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined
console.log('📋 Completion details:', {
aTag,
completed,
pubkey: event.pubkey,
eventId: event.id
})
const completion: EventCompletion = {
id: event.id,
eventAddress: aTag,
@ -139,6 +148,9 @@ export class ScheduledEventService extends BaseService {
const existing = this._completions.get(aTag)
if (!existing || event.created_at > existing.created_at) {
this._completions.set(aTag, completion)
console.log('✅ Stored completion for:', aTag, '- completed:', completed)
} else {
console.log('⏭️ Skipped older completion for:', aTag)
}
} catch (error) {
@ -228,9 +240,12 @@ export class ScheduledEventService extends BaseService {
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
// Publish the completion
await this.relayHub.publishEvent(signedEvent)
console.log('📤 Publishing completion event (kind 31925) for:', eventAddress)
const result = await this.relayHub.publishEvent(signedEvent)
console.log('✅ Completion event published to', result.success, '/', result.total, 'relays')
// Optimistically update local state
console.log('🔄 Optimistically updating local state')
this.handleCompletionEvent(signedEvent)
} catch (error) {