From d497cfa4d961574e8a9a946fac894f7e028882c2 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 16 Nov 2025 16:45:45 +0100 Subject: [PATCH] Implement task status workflow: claimed, in-progress, completed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added granular task state management to scheduled events/tasks with three states plus unclaimed. Tasks now support a full workflow from claiming to completion with visual feedback at each stage. **New Task States:** - **Unclaimed** (no RSVP event) - Task available for anyone to claim - **Claimed** - User has reserved the task but hasn't started - **In Progress** - User is actively working on the task - **Completed** - Task is done - **Blocked** - Task is stuck (supported but not yet used in UI) - **Cancelled** - Task won't be completed (supported but not yet used in UI) **Service Layer (ScheduledEventService.ts):** - Updated `EventCompletion` interface: replaced `completed: boolean` with `taskStatus: TaskStatus` - Added `TaskStatus` type: `'claimed' | 'in-progress' | 'completed' | 'blocked' | 'cancelled'` - New methods: `claimTask()`, `startTask()`, `getTaskStatus()` - Refactored `completeEvent()` and renamed `uncompleteEvent()` to `unclaimTask()` - Internal `updateTaskStatus()` method handles all state changes - Uses `task-status` tag instead of `completed` tag in Nostr events - `unclaimTask()` publishes deletion event (kind 5) to remove RSVP - Backward compatibility: reads old `completed` tag and converts to new taskStatus **Composable (useScheduledEvents.ts):** - Exported new methods: `claimTask`, `startTask`, `unclaimTask`, `getTaskStatus` - Updated `completeEvent` signature to accept occurrence parameter - Marked `toggleComplete` as deprecated (still works for compatibility) **UI (ScheduledEventCard.vue):** - Context-aware action buttons based on current task status: - Unclaimed: "Claim Task" button - Claimed: "Start Task" + "Unclaim" buttons - In Progress: "Mark Complete" + "Unclaim" buttons - Completed: "Unclaim" button only - Status badges with icons and color coding: - 👋 Claimed (blue) - 🔄 In Progress (orange) - ✓ Completed (green) - Shows who claimed/is working on/completed each task - Unified confirmation dialog for all actions - Quick action buttons in collapsed view - Full button set in expanded view **Feed Integration (NostrFeed.vue):** - Added handlers: `onClaimTask`, `onStartTask`, `onCompleteTask`, `onUnclaimTask` - Passes `getTaskStatus` prop to ScheduledEventCard - Wired up all new event emitters **Nostr Protocol:** - Uses NIP-52 Calendar Event RSVP (kind 31925) - Custom `task-status` tag for granular state tracking - Deletion events (kind 5) for unclaiming tasks - Fully decentralized - all state stored on Nostr relays 🐢 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../nostr-feed/components/NostrFeed.vue | 55 +++- .../components/ScheduledEventCard.vue | 260 +++++++++++++++--- .../composables/useScheduledEvents.ts | 102 ++++++- .../services/ScheduledEventService.ts | 135 ++++++--- 4 files changed, 452 insertions(+), 100 deletions(-) diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 9c431df..b3dfc22 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -99,7 +99,16 @@ const { getDisplayName, fetchProfiles } = useProfiles() const { getEventReactions, subscribeToReactions, toggleLike } = useReactions() // Use scheduled events service -const { getEventsForSpecificDate, getCompletion, toggleComplete, allCompletions } = useScheduledEvents() +const { + getEventsForSpecificDate, + getCompletion, + getTaskStatus, + claimTask, + startTask, + completeEvent, + unclaimTask, + allCompletions +} = useScheduledEvents() // Selected date for viewing scheduled tasks (defaults to today) const selectedDate = ref(new Date().toISOString().split('T')[0]) @@ -255,14 +264,40 @@ async function onToggleLike(note: FeedPost) { } } -// Handle scheduled event completion toggle -async function onToggleComplete(event: ScheduledEvent, occurrence?: string) { - console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title, 'occurrence:', occurrence) +// Task action handlers +async function onClaimTask(event: ScheduledEvent, occurrence?: string) { + console.log('👋 NostrFeed: Claiming task:', event.title) try { - await toggleComplete(event, occurrence) - console.log('✅ NostrFeed: toggleComplete succeeded') + await claimTask(event, '', occurrence) } catch (error) { - console.error('❌ NostrFeed: Failed to toggle event completion:', error) + console.error('❌ Failed to claim task:', error) + } +} + +async function onStartTask(event: ScheduledEvent, occurrence?: string) { + console.log('▶️ NostrFeed: Starting task:', event.title) + try { + await startTask(event, '', occurrence) + } catch (error) { + console.error('❌ Failed to start task:', error) + } +} + +async function onCompleteTask(event: ScheduledEvent, occurrence?: string) { + console.log('✅ NostrFeed: Completing task:', event.title) + try { + await completeEvent(event, occurrence, '') + } catch (error) { + console.error('❌ Failed to complete task:', error) + } +} + +async function onUnclaimTask(event: ScheduledEvent, occurrence?: string) { + console.log('🔙 NostrFeed: Unclaiming task:', event.title) + try { + await unclaimTask(event, occurrence) + } catch (error) { + console.error('❌ Failed to unclaim task:', error) } } @@ -514,8 +549,12 @@ function cancelDelete() { :event="event" :get-display-name="getDisplayName" :get-completion="getCompletion" + :get-task-status="getTaskStatus" :admin-pubkeys="adminPubkeys" - @toggle-complete="onToggleComplete" + @claim-task="onClaimTask" + @start-task="onStartTask" + @complete-task="onCompleteTask" + @unclaim-task="onUnclaimTask" />
diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index dfc48af..d6e4ef1 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -15,18 +15,22 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next' -import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService' +import { Calendar, MapPin, Clock, CheckCircle, PlayCircle, Hand } from 'lucide-vue-next' +import type { ScheduledEvent, EventCompletion, TaskStatus } from '../services/ScheduledEventService' interface Props { event: ScheduledEvent getDisplayName: (pubkey: string) => string getCompletion: (eventAddress: string, occurrence?: string) => EventCompletion | undefined + getTaskStatus: (eventAddress: string, occurrence?: string) => TaskStatus | null adminPubkeys?: string[] } interface Emits { - (e: 'toggle-complete', event: ScheduledEvent, occurrence?: string): void + (e: 'claim-task', event: ScheduledEvent, occurrence?: string): void + (e: 'start-task', event: ScheduledEvent, occurrence?: string): void + (e: 'complete-task', event: ScheduledEvent, occurrence?: string): void + (e: 'unclaim-task', event: ScheduledEvent, occurrence?: string): void } const props = withDefaults(defineProps(), { @@ -53,12 +57,29 @@ const occurrence = computed(() => { // Check if this is an admin event const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey)) -// Check if event is completed - call function with occurrence for recurring events -const isCompleted = computed(() => props.getCompletion(eventAddress.value, occurrence.value)?.completed || false) +// Get current task status +const taskStatus = computed(() => props.getTaskStatus(eventAddress.value, occurrence.value)) // Check if event is completable (task type) const isCompletable = computed(() => props.event.eventType === 'task') +// Get completion data +const completion = computed(() => props.getCompletion(eventAddress.value, occurrence.value)) + +// Status badges configuration +const statusConfig = computed(() => { + switch (taskStatus.value) { + case 'claimed': + return { label: 'Claimed', variant: 'secondary' as const, icon: Hand, color: 'text-blue-600' } + case 'in-progress': + return { label: 'In Progress', variant: 'default' as const, icon: PlayCircle, color: 'text-orange-600' } + case 'completed': + return { label: 'Completed', variant: 'secondary' as const, icon: CheckCircle, color: 'text-green-600' } + default: + return null + } +}) + // Format the date/time const formattedDate = computed(() => { try { @@ -110,28 +131,102 @@ const formattedTimeRange = computed(() => { } }) -// Handle mark complete button click - show confirmation dialog -function handleMarkComplete() { - console.log('🔘 Mark Complete button clicked for event:', props.event.title) +// Action type for confirmation dialog +const pendingAction = ref<'claim' | 'start' | 'complete' | 'unclaim' | null>(null) + +// Handle claim task +function handleClaimTask() { + pendingAction.value = 'claim' showConfirmDialog.value = true } -// Confirm and execute mark complete -function confirmMarkComplete() { - console.log('✅ Confirmed mark complete for event:', props.event.title, 'occurrence:', occurrence.value) - emit('toggle-complete', props.event, occurrence.value) - showConfirmDialog.value = false +// Handle start task +function handleStartTask() { + pendingAction.value = 'start' + showConfirmDialog.value = true } -// Cancel mark complete -function cancelMarkComplete() { - showConfirmDialog.value = false +// Handle complete task +function handleCompleteTask() { + pendingAction.value = 'complete' + showConfirmDialog.value = true } + +// Handle unclaim task +function handleUnclaimTask() { + pendingAction.value = 'unclaim' + showConfirmDialog.value = true +} + +// Confirm action +function confirmAction() { + if (!pendingAction.value) return + + switch (pendingAction.value) { + case 'claim': + emit('claim-task', props.event, occurrence.value) + break + case 'start': + emit('start-task', props.event, occurrence.value) + break + case 'complete': + emit('complete-task', props.event, occurrence.value) + break + case 'unclaim': + emit('unclaim-task', props.event, occurrence.value) + break + } + + showConfirmDialog.value = false + pendingAction.value = null +} + +// Cancel action +function cancelAction() { + showConfirmDialog.value = false + pendingAction.value = null +} + +// Get dialog content based on pending action +const dialogContent = computed(() => { + switch (pendingAction.value) { + case 'claim': + return { + title: 'Claim Task?', + description: `This will mark "${props.event.title}" as claimed by you. You can start working on it later.`, + confirmText: 'Claim Task' + } + case 'start': + return { + title: 'Start Task?', + description: `This will mark "${props.event.title}" as in-progress. Others will see you're actively working on it.`, + confirmText: 'Start Task' + } + case 'complete': + return { + title: 'Complete Task?', + description: `This will mark "${props.event.title}" as completed by you. Other users will be able to see that you completed this task.`, + confirmText: 'Mark Complete' + } + case 'unclaim': + return { + title: 'Unclaim Task?', + description: `This will remove your claim on "${props.event.title}" and make it available for others.`, + confirmText: 'Unclaim Task' + } + default: + return { + title: '', + description: '', + confirmText: '' + } + } +})