From 25fa76e59dfd69b7e5d434dedebd9dd3d5ffc7bd Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Feb 2026 13:19:52 -0600 Subject: [PATCH] Filter out completed sprints from task create/edit dropdowns --- scripts/README.md | 688 +++++++++++++++++--------------- scripts/examples/task-full.json | 66 +++ scripts/project.sh | 376 +++++++++++++++++ scripts/sprint.sh | 511 ++++++++++++++++++++++++ scripts/task.sh | 1 + src/app/page.tsx | 26 +- src/app/tasks/[taskId]/page.tsx | 40 +- 7 files changed, 1348 insertions(+), 360 deletions(-) create mode 100644 scripts/examples/task-full.json create mode 100755 scripts/project.sh create mode 100755 scripts/sprint.sh create mode 100755 scripts/task.sh diff --git a/scripts/README.md b/scripts/README.md index 5cb6925..f60cca7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,339 +1,377 @@ -# Gantt Board CLI Tools +# Gantt Board CLI -Complete command-line interface for the Gantt Board. All web UI operations available via CLI. +A comprehensive command-line interface for managing tasks, projects, and sprints in the Gantt Board system. -## Quick Start +## Scripts +- `task.sh` - Full CRUD operations for tasks +- `project.sh` - Project management +- `sprint.sh` - Sprint management + +## Installation + +1. Make scripts executable: ```bash -# Pull latest schema from Supabase into supabase/schema.sql -SUPABASE_DB_URL='postgresql://...' ./scripts/pull-supabase-schema.sh - -# List all tasks -./scripts/gantt.sh task list - -# Create a task -./scripts/gantt.sh task create "Fix login bug" open high - -# Create task with natural language -./scripts/gantt.sh task natural "Research TTS options by Friday, high priority" - -# Update task -./scripts/gantt.sh task update status done - -# Add comment -./scripts/gantt.sh task comment "Working on this now" - -# Attach file -./scripts/gantt.sh task attach ./notes.md +chmod +x task.sh project.sh sprint.sh ``` -## Schema Dump Script - -Use `pull-supabase-schema.sh` when `supabase/schema.sql` needs to match the live database: - -```bash -SUPABASE_DB_URL='postgresql://postgres:***@db..supabase.co:5432/postgres?sslmode=require' \ - ./scripts/pull-supabase-schema.sh -``` - -Optional: - -```bash -# Include additional schemas -SCHEMAS='public,auth,storage' \ -SUPABASE_DB_URL='postgresql://...' \ - ./scripts/pull-supabase-schema.sh supabase/full-schema.sql -``` - -## Main CLI: `gantt.sh` - -A unified CLI that covers all API operations. - -### Task Commands - -```bash -# List tasks (optionally filter by status) -./scripts/gantt.sh task list -./scripts/gantt.sh task list open -./scripts/gantt.sh task list in-progress - -# Get specific task -./scripts/gantt.sh task get - -# Create task -./scripts/gantt.sh task create [status] [priority] [project-id] -./scripts/gantt.sh task create "Fix bug" open high <project-uuid> - -# Create from natural language -./scripts/gantt.sh task natural "Fix login bug by Friday, assign to Matt, high priority" - -# Update any field -./scripts/gantt.sh task update <task-id> <field> <value> -./scripts/gantt.sh task update <task-uuid> status done -./scripts/gantt.sh task update <task-uuid> priority urgent -./scripts/gantt.sh task update <task-uuid> title "New title" -./scripts/gantt.sh task update <task-uuid> assigneeId <user-uuid> - -# Delete task -./scripts/gantt.sh task delete <task-id> - -# Add comment -./scripts/gantt.sh task comment <task-id> "Your comment here" - -# Attach file -./scripts/gantt.sh task attach <task-id> <file-path> -./scripts/gantt.sh task attach <task-uuid> ./research.pdf -``` - -Comment payload persisted by the API/CLI: - -```json -{ - "id": "string", - "text": "string", - "createdAt": "2026-02-23T19:00:00.000Z", - "commentAuthorId": "user-uuid-or-assistant", - "replies": [] -} -``` - -- `commentAuthorId` is the only supported author field. - -### Project Commands - -```bash -# List all projects -./scripts/gantt.sh project list - -# Get specific project -./scripts/gantt.sh project get <project-id> - -# Create project -./scripts/gantt.sh project create "Project Name" "Description" "#color" - -# Update project field -./scripts/gantt.sh project update <project-id> <field> <value> -./scripts/gantt.sh project update <project-uuid> name "New Name" -./scripts/gantt.sh project update <project-uuid> description "New desc" -./scripts/gantt.sh project update <project-uuid> color "#ff0000" - -# Delete project -./scripts/gantt.sh project delete <project-id> -``` - -### Sprint Commands - -```bash -# List all sprints -./scripts/gantt.sh sprint list - -# Get specific sprint -./scripts/gantt.sh sprint get <sprint-id> - -# Create sprint -./scripts/gantt.sh sprint create "Sprint 2" <project-id> "2026-02-23" "2026-03-01" "Sprint goal" - -# Update sprint field -./scripts/gantt.sh sprint update <sprint-id> <field> <value> -./scripts/gantt.sh sprint update <sprint-uuid> name "New Sprint Name" -./scripts/gantt.sh sprint update <sprint-uuid> status active -./scripts/gantt.sh sprint update <sprint-uuid> startDate "2026-02-25" - -# Delete sprint -./scripts/gantt.sh sprint delete <sprint-id> -``` - -### Auth Commands - -```bash -# Check current session -./scripts/gantt.sh auth session - -# Log in -./scripts/gantt.sh auth login <email> <password> - -# Log out -./scripts/gantt.sh auth logout - -# Register new account -./scripts/gantt.sh auth register <email> <password> [name] - -# Request password reset -./scripts/gantt.sh auth forgot-password <email> - -# Reset password with token -./scripts/gantt.sh auth reset-password <token> <new-password> - -# Update account -./scripts/gantt.sh auth account <field> <value> - -# List all users -./scripts/gantt.sh auth users -``` - -### Debug - -```bash -# Call debug endpoint -./scripts/gantt.sh debug -``` - -## Legacy Scripts (Supabase Direct) - -These scripts use Supabase directly (not the API) and work without the web server running: - -### `gantt-task-crud.sh` - Direct Supabase Task Operations - -Uses Supabase REST API directly with service role key. - -```bash -./scripts/gantt-task-crud.sh list # List all tasks -./scripts/gantt-task-crud.sh list open # List open tasks -./scripts/gantt-task-crud.sh get <task-id> # Get specific task -./scripts/gantt-task-crud.sh create "Title" # Create task -./scripts/gantt-task-crud.sh update <id> field value # Update field -./scripts/gantt-task-crud.sh delete <task-id> # Delete task -``` - -### `attach-file.sh` - Direct Supabase File Attachments - -```bash -./scripts/attach-file.sh <task-id> <file-path> -``` - -Supports: md, txt, json, pdf, png, jpg, gif - -### `view-attachment.sh` - View Attached Files - -```bash -./scripts/view-attachment.sh <task-id> [index] -``` - -Displays text files in terminal, saves binary files to `/tmp/`. - -## API Coverage Matrix - -| Feature | Web UI | gantt.sh (API) | Legacy Scripts (Supabase) | -|---------|--------|----------------|---------------------------| -| List tasks | ✅ | ✅ | ✅ | -| Get task | ✅ | ✅ | ✅ | -| Create task | ✅ | ✅ | ✅ | -| Natural language create | ✅ | ✅ | ❌ | -| Update task | ✅ | ✅ | ✅ | -| Delete task | ✅ | ✅ | ✅ | -| Add comment | ✅ | ✅ | ❌ | -| Attach file | ✅ | ✅ | ✅ | -| List projects | ✅ | ✅ | ❌ | -| Get project | ✅ | ✅ | ❌ | -| Create project | ✅ | ✅ | ❌ | -| Update project | ✅ | ✅ | ❌ | -| Delete project | ✅ | ✅ | ❌ | -| List sprints | ✅ | ✅ | ❌ | -| Get sprint | ✅ | ✅ | ❌ | -| Create sprint | ✅ | ✅ | ❌ | -| Update sprint | ✅ | ✅ | ❌ | -| Delete sprint | ✅ | ✅ | ❌ | -| Auth login | ✅ | ✅ | ❌ | -| Auth logout | ✅ | ✅ | ❌ | -| Auth register | ✅ | ✅ | ❌ | -| Auth forgot-password | ✅ | ✅ | ❌ | -| Auth reset-password | ✅ | ✅ | ❌ | -| Auth account update | ✅ | ✅ | ❌ | -| Auth list users | ✅ | ✅ | ❌ | -| View attachments | ✅ | ❌ | ✅ | - -### Auditing Coverage - -Use the audit script to verify CLI stays in sync with API: - -```bash -./scripts/audit-cli-coverage.sh -``` - -This scans all API routes and checks that each has a matching CLI command. Run this before committing any API changes to ensure Rule 2.5 compliance. - -## Requirements - -- `curl` - HTTP requests (built into macOS) -- `jq` - JSON parsing: `brew install jq` - -## Environment Variables - -```bash -# Override API URL -export API_URL=http://localhost:3001/api -./scripts/gantt.sh task list -``` - -## Common Workflows - -### Create Task with Natural Language - -```bash -./scripts/gantt.sh task natural "Fix the login bug by Friday, high priority, assign to Matt" -``` - -### Complete Task with Documentation - -```bash -# Add completion comment -./scripts/gantt.sh task comment <task-uuid> "Completed. See attached notes." - -# Attach notes -./scripts/gantt.sh task attach <task-uuid> ./completion-notes.md - -# Mark done -./scripts/gantt.sh task update <task-uuid> status done -``` - -### Find Tasks - -```bash -# All open tasks -./scripts/gantt.sh task list open - -# Filter with jq -./scripts/gantt.sh task list | jq '.[] | select(.priority == "urgent") | {id, title}' -``` - -## Which Script to Use? - -**Use `gantt.sh` (API) when:** -- Web server is running -- You want natural language task creation -- You need to add comments -- You want to use the same API as the web UI - -**Use legacy scripts (Supabase) when:** -- Web server is not running -- You need to view attachment content -- You want direct database access - -## Troubleshooting - -### "jq: command not found" +2. Ensure dependencies are installed: ```bash brew install jq ``` -### "API call failed (HTTP 401)" -- Check that you're logged in: `./scripts/gantt.sh auth session` -- Log in: `./scripts/gantt.sh auth login <email> <password>` +## Configuration -### "API call failed (HTTP 500)" -- Check that the dev server is running: `npm run dev` -- Check server logs for errors +Scripts use the following defaults: +- **Supabase URL**: `https://qnatchrjlpehiijwtreh.supabase.co` +- **Default Project**: OpenClaw iOS +- **Default Assignee**: Max -### Task ID format -Task IDs are UUIDs like `33ebc71e-7d40-456c-8f98-bb3578d2bb2b`. Find them: -- In the URL when viewing a task -- From `task list` output -- From `task create` output -- `projectId`, `sprintId`, and `assigneeId` fields are UUID values as well. +## Task Management (`task.sh`) -## Tips +### Create Task -1. **Tab completion:** Add `source <(./scripts/gantt.sh completion bash)` to your `.bashrc` -2. **jq filtering:** Pipe output to `jq` for precise data extraction -3. **Task URLs:** `https://gantt-board.vercel.app/tasks/<task-id>` -4. **Always verify:** After attach/update, run `task get` to confirm +```bash +# Minimal task +./task.sh create --title "Fix bug" + +# Full task with all fields +./task.sh create \ + --title "Implement OAuth" \ + --description "Add OAuth2 login support" \ + --type task \ + --status todo \ + --priority high \ + --project "Gantt Board" \ + --sprint "current" \ + --assignee "Max" \ + --due-date "2026-03-01" \ + --tags "auth,security" \ + --comments "Starting implementation" + +# Create from JSON file +./task.sh create --file task.json + +# Interactive mode +./task.sh create --interactive + +# Auto-create project if not found +./task.sh create --title "New Feature" --project "New Project" --auto-create +``` + +### List Tasks + +```bash +# List all tasks +./task.sh list + +# Filter by status +./task.sh list --status todo + +# Filter by priority +./task.sh list --priority high + +# Filter by project (auto-resolves name) +./task.sh list --project "Gantt Board" + +# Filter by assignee +./task.sh list --assignee "Max" + +# Filter by type +./task.sh list --type bug + +# JSON output +./task.sh list --json + +# Limit results +./task.sh list --limit 10 +``` + +### Get Task + +```bash +./task.sh get <task-id> +``` + +### Update Task + +```bash +# Update status +./task.sh update <task-id> --status in-progress + +# Update multiple fields +./task.sh update <task-id> \ + --status done \ + --priority low \ + --add-comment "Task completed" + +# Update assignee +./task.sh update <task-id> --assignee "Matt" + +# Clear tags +./task.sh update <task-id> --clear-tags + +# Update tags +./task.sh update <task-id> --tags "new-tag,another-tag" +``` + +### Delete Task + +```bash +./task.sh delete <task-id> +``` + +### Bulk Create + +```bash +# From JSON file +./task.sh bulk-create tasks.json + +# With auto-create for projects +./task.sh bulk-create tasks.json --auto-create +``` + +**JSON Format:** +```json +[ + { + "title": "Task 1", + "description": "Description", + "type": "task", + "status": "todo", + "priority": "high", + "project": "Project Name", + "sprint": "Sprint 1", + "assignee": "Max", + "due_date": "2026-03-01", + "tags": "tag1,tag2", + "comments": "Initial comment" + } +] +``` + +## Project Management (`project.sh`) + +### Create Project + +```bash +./project.sh create --name "New Project" --description "Project description" --color "#3b82f6" +``` + +### List Projects + +```bash +# List all projects +./project.sh list + +# JSON output +./project.sh list --json +``` + +### Get Project + +```bash +# By ID +./project.sh get <project-id> + +# By name (auto-resolves) +./project.sh get "Gantt Board" +``` + +### Update Project + +```bash +./project.sh update <project-id-or-name> \ + --name "Updated Name" \ + --description "Updated description" \ + --color "#ff0000" +``` + +### Delete Project + +```bash +./project.sh delete <project-id-or-name> +``` + +## Sprint Management (`sprint.sh`) + +### Create Sprint + +```bash +./sprint.sh create \ + --name "Sprint 3" \ + --project "Gantt Board" \ + --goal "Complete API integration" \ + --start-date "2026-02-24" \ + --end-date "2026-03-07" +``` + +### List Sprints + +```bash +# List all sprints +./sprint.sh list + +# List active sprints +./sprint.sh list --active + +# Filter by project +./sprint.sh list --project "Gantt Board" + +# JSON output +./sprint.sh list --json +``` + +### Get Sprint + +```bash +# By ID +./sprint.sh get <sprint-id> + +# By name +./sprint.sh get "Sprint 1" +``` + +### Update Sprint + +```bash +./sprint.sh update <sprint-id-or-name> \ + --name "Updated Sprint" \ + --goal "Updated goal" \ + --status active \ + --start-date "2026-02-25" \ + --end-date "2026-03-10" +``` + +### Close Sprint + +```bash +./sprint.sh close <sprint-id-or-name> +``` + +### Delete Sprint + +```bash +./sprint.sh delete <sprint-id-or-name> +``` + +## Name Resolution + +All scripts support automatic name-to-ID resolution: + +- **Projects**: "Gantt Board" → `68d05855-a399-44a4-9c36-3ee78257c2c9` +- **Sprints**: "Sprint 1" → `b2c3d4e5-0001-0000-0000-000000000001` +- **Assignees**: + - "Max" → `9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa` + - "Matt" → `0a3e400c-3932-48ae-9b65-f3f9c6f26fe9` +- **Special**: Use "current" for the most recent active sprint + +## Task Types + +- `task` - Standard task +- `bug` - Bug fix +- `research` - Research spike +- `plan` - Planning task +- `idea` - Idea/backlog item + +## Task Statuses + +- `open` - Newly created +- `todo` - Ready to start +- `blocked` - Blocked +- `in-progress` - Currently working +- `review` - Ready for review +- `validate` - Needs validation +- `done` - Completed + +## Priorities + +- `low` +- `medium` +- `high` +- `urgent` + +## Examples + +### Daily Workflow + +```bash +# Create a new task +./task.sh create --title "Fix login bug" --type bug --priority urgent --project "Gantt Board" + +# List my high priority tasks +./task.sh list --assignee "Max" --priority high + +# Update task status +./task.sh update <task-id> --status in-progress + +# Add progress comment +./task.sh update <task-id> --add-comment "Working on reproduction steps" + +# Mark as done +./task.sh update <task-id> --status done --add-comment "Fixed in commit abc123" +``` + +### Sprint Planning + +```bash +# Create new sprint +./sprint.sh create --name "Sprint 5" --project "Gantt Board" --start-date 2026-03-01 --end-date 2026-03-14 + +# Create multiple tasks for the sprint +./task.sh bulk-create sprint-tasks.json + +# Close previous sprint +./sprint.sh close "Sprint 4" +``` + +### Project Setup + +```bash +# Create new project +./project.sh create --name "Mobile App" --description "iOS and Android app" --color "#8b5cf6" + +# Create initial sprint +./sprint.sh create --name "Sprint 1" --project "Mobile App" --goal "MVP release" + +# Create starter tasks +./task.sh create --title "Setup repo" --project "Mobile App" --sprint "Sprint 1" +./task.sh create --title "Design system" --project "Mobile App" --sprint "Sprint 1" +``` + +## Exit Codes + +- `0` - Success +- `1` - Error (invalid input, API failure, not found) + +## Environment Variables + +Scripts use hardcoded configuration at the top of each file. To customize, edit: +- `SUPABASE_URL` - Supabase instance URL +- `SERVICE_KEY` - Supabase service role key +- `DEFAULT_PROJECT_ID` - Default project for tasks +- `DEFAULT_ASSIGNEE_ID` - Default assignee for tasks +- `MATT_ID` - Matt's user ID + +## Troubleshooting + +### "Project not found" +- Use `--auto-create` flag to create project automatically +- Check project name spelling +- Use `./project.sh list` to see available projects + +### "Sprint not found" +- Use "current" to reference the most recent active sprint +- Check sprint name with `./sprint.sh list` + +### "jq parse error" +- Ensure `jq` is installed: `brew install jq` +- Check JSON file syntax + +### Empty response on create +- This is normal - Supabase returns empty on successful POST +- Check list command to verify creation: `./task.sh list --limit 5` + +## License + +Part of the OpenClaw Gantt Board project. diff --git a/scripts/examples/task-full.json b/scripts/examples/task-full.json new file mode 100644 index 0000000..b009669 --- /dev/null +++ b/scripts/examples/task-full.json @@ -0,0 +1,66 @@ +[ + { + "title": "Implement user authentication", + "description": "Add OAuth2-based authentication with Google and GitHub providers. Include JWT token handling and refresh token mechanism.", + "type": "task", + "status": "todo", + "priority": "high", + "project": "Gantt Board", + "sprint": "current", + "assignee": "Max", + "due_date": "2026-03-01", + "tags": ["auth", "security", "backend"], + "comments": "Initial research completed. Ready to implement." + }, + { + "title": "Fix drag-and-drop performance", + "description": "Tasks stutter when dragging in the Gantt board with >50 items. Need to optimize rendering and use virtual scrolling.", + "type": "bug", + "status": "open", + "priority": "urgent", + "project": "Gantt Board", + "sprint": "current", + "assignee": "Max", + "due_date": "2026-02-28", + "tags": ["performance", "ui", "frontend"], + "comments": "Reported by multiple users. Priority fix needed." + }, + { + "title": "Evaluate React Query vs SWR", + "description": "Compare React Query and SWR for data fetching. Document pros/cons and migration path.", + "type": "research", + "status": "open", + "priority": "medium", + "project": "Gantt Board", + "sprint": "current", + "assignee": "Matt", + "due_date": "2026-03-05", + "tags": ["research", "architecture"], + "comments": "Part of the Q1 tech stack review." + }, + { + "title": "Q2 roadmap planning", + "description": "Create detailed roadmap for Q2 including milestones, resource allocation, and key deliverables.", + "type": "plan", + "status": "todo", + "priority": "high", + "project": "Gantt Board", + "sprint": "current", + "assignee": "Max", + "due_date": "2026-03-10", + "tags": ["planning", "roadmap"], + "comments": "Due before end of Q1 review meeting." + }, + { + "title": "Dark mode theme", + "description": "Implement dark mode toggle with proper contrast ratios and system preference detection.", + "type": "idea", + "status": "open", + "priority": "low", + "project": "Gantt Board", + "sprint": "", + "assignee": "Matt", + "tags": ["ui", "ux", "feature"], + "comments": "Nice to have feature for accessibility." + } +] diff --git a/scripts/project.sh b/scripts/project.sh new file mode 100755 index 0000000..0355d15 --- /dev/null +++ b/scripts/project.sh @@ -0,0 +1,376 @@ +#!/bin/bash +# +# Project CLI for Gantt Board +# CRUD operations for projects +# Usage: ./project.sh [create|list|get|update|delete] [options] + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co" +SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0" +HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json") + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper Functions +log_info() { echo -e "${BLUE}ℹ${NC} $1"; } +log_success() { echo -e "${GREEN}✓${NC} $1"; } +log_warning() { echo -e "${YELLOW}⚠${NC} $1"; } +log_error() { echo -e "${RED}✗${NC} $1"; } + +# Check dependencies +check_dependencies() { + if ! command -v jq &> /dev/null; then + log_error "jq is required but not installed. Install with: brew install jq" + exit 1 + fi + if ! command -v uuidgen &> /dev/null; then + log_error "uuidgen is required but not installed." + exit 1 + fi +} + +# Generate UUID +generate_uuid() { + uuidgen | tr '[:upper:]' '[:lower:]' +} + +# Get current timestamp ISO format +get_timestamp() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +# Show usage +show_usage() { + cat << EOF +Project CLI for Gantt Board + +USAGE: + ./project.sh [COMMAND] [OPTIONS] + +COMMANDS: + create Create a new project + list List all projects + get <id> Get a specific project by ID or name + update <id> Update a project + delete <id> Delete a project + +CREATE OPTIONS: + --name "Name" Project name (required) + --description "Description" Project description + --color "#hexcolor" Project color for UI + +LIST OPTIONS: + --json Output as JSON + +UPDATE OPTIONS: + --name "Name" + --description "Description" + --color "#hexcolor" + +EXAMPLES: + # Create project + ./project.sh create --name "Web Projects" --description "All web development work" + + # List all active projects + ./project.sh list + + # Get project by ID + ./project.sh get a1b2c3d4-0001-0000-0000-000000000001 + + # Update project + ./project.sh update a1b2c3d4-0001-0000-0000-000000000001 --status archived + + # Delete project + ./project.sh delete a1b2c3d4-0001-0000-0000-000000000001 + +EOF +} + +# Resolve project name to ID +resolve_project_id() { + local identifier="$1" + + # Check if it's already a UUID + if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then + echo "$identifier" + return 0 + fi + + # Search by name (case-insensitive) + local project_id + project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$project_id" ]]; then + echo "$project_id" + return 0 + fi + + # Try exact match + local encoded_name + encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri) + project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$project_id" ]]; then + echo "$project_id" + return 0 + fi + + log_error "Project '$identifier' not found" + return 1 +} + +# Create command +cmd_create() { + local name="" + local description="" + local color="#3b82f6" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --name) name="$2"; shift 2 ;; + --description) description="$2"; shift 2 ;; + --color) color="$2"; shift 2 ;; + *) shift ;; + esac + done + + # Validate required fields + if [[ -z "$name" ]]; then + log_error "Name is required (use --name)" + exit 1 + fi + + # Generate project data + local project_id + project_id=$(generate_uuid) + local timestamp + timestamp=$(get_timestamp) + + # Build JSON payload + local json_payload + json_payload=$(jq -n \ + --arg id "$project_id" \ + --arg name "$name" \ + --arg color "$color" \ + --arg created_at "$timestamp" \ + '{ + id: $id, + name: $name, + color: $color, + created_at: $created_at + }') + + # Add optional fields + if [[ -n "$description" ]]; then + json_payload=$(echo "$json_payload" | jq --arg v "$description" '. + {description: $v}') + fi + + # Create project + log_info "Creating project..." + local response + response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/projects" \ + "${HEADERS[@]}" \ + -d "$json_payload") + + # Supabase returns empty on success, or error JSON on failure + if [[ -z "$response" ]]; then + log_success "Project created: $project_id" + elif [[ "$response" == *"error"* ]] || [[ "$response" == *""*"code"* ]]; then + log_error "Failed to create project" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + else + log_success "Project created: $project_id" + echo "$response" | jq . 2>/dev/null || echo "$response" + fi +} + +# List command +cmd_list() { + local output_json=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --json) output_json=true; shift ;; + *) shift ;; + esac + done + + # Build query + local query="${SUPABASE_URL}/rest/v1/projects?select=*&order=created_at.desc" + + log_info "Fetching projects..." + local response + response=$(curl -s "$query" "${HEADERS[@]}") + + if [[ "$output_json" == true ]]; then + echo "$response" | jq . + else + # Table output + local count + count=$(echo "$response" | jq 'length') + log_success "Found $count project(s)" + + # Print header + printf "%-36s %-25s %-30s\n" "ID" "NAME" "DESCRIPTION" + printf "%-36s %-25s %-30s\n" "------------------------------------" "-------------------------" "------------------------------" + + # Print rows + echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.description // "" | tostring | .[0:28])] | @tsv' | while IFS=$'\t' read -r id name desc; do + printf "%-36s %-25s %-30s\n" "$id" "$name" "$desc" + done + fi +} + +# Get command +cmd_get() { + local identifier="$1" + + if [[ -z "$identifier" ]]; then + log_error "Project ID or name required. Usage: ./project.sh get <id-or-name>" + exit 1 + fi + + local project_id + project_id=$(resolve_project_id "$identifier") + + log_info "Fetching project $project_id..." + local response + response=$(curl -s "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}&select=*" \ + "${HEADERS[@]}") + + local project + project=$(echo "$response" | jq '.[0] // empty') + + if [[ -n "$project" && "$project" != "{}" && "$project" != "null" ]]; then + echo "$project" | jq . + else + log_error "Project not found: $identifier" + exit 1 + fi +} + +# Update command +cmd_update() { + local identifier="$1" + shift + + if [[ -z "$identifier" ]]; then + log_error "Project ID or name required. Usage: ./project.sh update <id-or-name> [options]" + exit 1 + fi + + local project_id + project_id=$(resolve_project_id "$identifier") + + # Build update payload + local update_fields="{}" + + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;; + --description) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;; + --color) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {color: $v}'); shift 2 ;; + *) shift ;; + esac + done + + # Check if we have anything to update + if [[ "$update_fields" == "{}" ]]; then + log_warning "No update fields specified" + exit 0 + fi + + # Add updated_at timestamp + local timestamp + timestamp=$(get_timestamp) + update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}') + + log_info "Updating project $project_id..." + local response + response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \ + "${HEADERS[@]}" \ + -d "$update_fields") + + if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then + log_success "Project updated: $project_id" + else + log_error "Failed to update project" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + fi +} + +# Delete command +cmd_delete() { + local identifier="$1" + + if [[ -z "$identifier" ]]; then + log_error "Project ID or name required. Usage: ./project.sh delete <id-or-name>" + exit 1 + fi + + local project_id + project_id=$(resolve_project_id "$identifier") + + log_info "Deleting project $project_id..." + local response + response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \ + "${HEADERS[@]}") + + if [[ -z "$response" || "$response" == "[]" ]]; then + log_success "Project deleted: $project_id" + else + log_error "Failed to delete project" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + fi +} + +# Main execution +check_dependencies + +# Parse command +COMMAND="${1:-}" +shift || true + +case "$COMMAND" in + create) + cmd_create "$@" + ;; + list) + cmd_list "$@" + ;; + get) + cmd_get "$@" + ;; + update) + cmd_update "$@" + ;; + delete) + cmd_delete "$@" + ;; + help|--help|-h) + show_usage + ;; + "") + show_usage + ;; + *) + log_error "Unknown command: $COMMAND" + show_usage + exit 1 + ;; +esac diff --git a/scripts/sprint.sh b/scripts/sprint.sh new file mode 100755 index 0000000..42d2ee8 --- /dev/null +++ b/scripts/sprint.sh @@ -0,0 +1,511 @@ +#!/bin/bash +# +# Sprint CLI for Gantt Board +# CRUD operations for sprints +# Usage: ./sprint.sh [create|list|get|update|delete|close] [options] + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co" +SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0" +HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json") + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper Functions +log_info() { echo -e "${BLUE}ℹ${NC} $1"; } +log_success() { echo -e "${GREEN}✓${NC} $1"; } +log_warning() { echo -e "${YELLOW}⚠${NC} $1"; } +log_error() { echo -e "${RED}✗${NC} $1"; } + +# Check dependencies +check_dependencies() { + if ! command -v jq &> /dev/null; then + log_error "jq is required but not installed. Install with: brew install jq" + exit 1 + fi + if ! command -v uuidgen &> /dev/null; then + log_error "uuidgen is required but not installed." + exit 1 + fi +} + +# Generate UUID +generate_uuid() { + uuidgen | tr '[:upper:]' '[:lower:]' +} + +# Get current timestamp ISO format +get_timestamp() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +# Show usage +show_usage() { + cat << EOF +Sprint CLI for Gantt Board + +USAGE: + ./sprint.sh [COMMAND] [OPTIONS] + +COMMANDS: + create Create a new sprint + list List all sprints + get <id> Get a specific sprint by ID or name + update <id> Update a sprint + delete <id> Delete a sprint + close <id> Close/complete a sprint + +CREATE OPTIONS: + --name "Name" Sprint name (required) + --project "Project" Project name or ID (required) + --goal "Goal" Sprint goal/description + --start-date "YYYY-MM-DD" Start date + --end-date "YYYY-MM-DD" End date + --status [planning|active|closed|completed] Status (default: planning) + +LIST OPTIONS: + --status <status> Filter by status + --project <project> Filter by project name/ID + --active Show only active sprints + --json Output as JSON + +UPDATE OPTIONS: + --name "Name" + --goal "Goal" + --start-date "YYYY-MM-DD" + --end-date "YYYY-MM-DD" + --status <status> + +EXAMPLES: + # Create sprint + ./sprint.sh create --name "Sprint 1" --project "Web Projects" \ + --goal "Complete MVP features" --start-date 2026-02-24 --end-date 2026-03-07 + + # List active sprints + ./sprint.sh list --active + + # Get sprint by name + ./sprint.sh get "Sprint 1" + + # Close a sprint (marks as completed) + ./sprint.sh close "Sprint 1" + + # Delete sprint + ./sprint.sh delete <sprint-id> + +EOF +} + +# Resolve project name to ID +resolve_project_id() { + local identifier="$1" + + # Check if it's already a UUID + if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then + echo "$identifier" + return 0 + fi + + # Search by name (case-insensitive) + local project_id + project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$project_id" ]]; then + echo "$project_id" + return 0 + fi + + # Try exact match + local encoded_name + encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri) + project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$project_id" ]]; then + echo "$project_id" + return 0 + fi + + log_error "Project '$identifier' not found" + return 1 +} + +# Resolve sprint identifier to ID +resolve_sprint_id() { + local identifier="$1" + + # Check if it's already a UUID + if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then + echo "$identifier" + return 0 + fi + + # Search by name (case-insensitive) + local sprint_id + sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=ilike.*${identifier}*&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$sprint_id" ]]; then + echo "$sprint_id" + return 0 + fi + + # Try exact match + local encoded_name + encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri) + sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=eq.${encoded_name}&select=id" \ + "${HEADERS[@]}" | jq -r '.[0].id // empty') + + if [[ -n "$sprint_id" ]]; then + echo "$sprint_id" + return 0 + fi + + log_error "Sprint '$identifier' not found" + return 1 +} + +# Create command +cmd_create() { + local name="" + local project="" + local goal="" + local start_date="" + local end_date="" + local status="planning" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --name) name="$2"; shift 2 ;; + --project) project="$2"; shift 2 ;; + --goal) goal="$2"; shift 2 ;; + --start-date) start_date="$2"; shift 2 ;; + --end-date) end_date="$2"; shift 2 ;; + --status) status="$2"; shift 2 ;; + *) shift ;; + esac + done + + # Validate required fields + if [[ -z "$name" ]]; then + log_error "Name is required (use --name)" + exit 1 + fi + + if [[ -z "$project" ]]; then + log_error "Project is required (use --project)" + exit 1 + fi + + # Resolve project + local project_id + project_id=$(resolve_project_id "$project") + + # Generate sprint data + local sprint_id + sprint_id=$(generate_uuid) + local timestamp + timestamp=$(get_timestamp) + + # Build JSON payload + local json_payload + json_payload=$(jq -n \ + --arg id "$sprint_id" \ + --arg name "$name" \ + --arg project_id "$project_id" \ + --arg status "$status" \ + --arg created_at "$timestamp" \ + '{ + id: $id, + name: $name, + project_id: $project_id, + status: $status, + created_at: $created_at + }') + + # Add optional fields + if [[ -n "$goal" ]]; then + json_payload=$(echo "$json_payload" | jq --arg v "$goal" '. + {goal: $v}') + fi + + if [[ -n "$start_date" ]]; then + json_payload=$(echo "$json_payload" | jq --arg v "$start_date" '. + {start_date: $v}') + fi + + if [[ -n "$end_date" ]]; then + json_payload=$(echo "$json_payload" | jq --arg v "$end_date" '. + {end_date: $v}') + fi + + # Create sprint + log_info "Creating sprint..." + local response + response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/sprints" \ + "${HEADERS[@]}" \ + -d "$json_payload") + + # Supabase returns empty on success, or error JSON on failure + if [[ -z "$response" ]]; then + log_success "Sprint created: $sprint_id" + elif [[ "$response" == *"error"* ]] || [[ "$response" == *'"code"'* ]]; then + log_error "Failed to create sprint" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + else + log_success "Sprint created: $sprint_id" + echo "$response" | jq . 2>/dev/null || echo "$response" + fi +} + +# List command +cmd_list() { + local filter_status="" + local filter_project="" + local active_only=false + local output_json=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --status) filter_status="$2"; shift 2 ;; + --project) filter_project="$2"; shift 2 ;; + --active) active_only=true; shift ;; + --json) output_json=true; shift ;; + *) shift ;; + esac + done + + # Build query + local query="${SUPABASE_URL}/rest/v1/sprints?select=*,projects(name)&order=created_at.desc" + + if [[ "$active_only" == true ]]; then + filter_status="active" + fi + + if [[ -n "$filter_status" ]]; then + local encoded_status + encoded_status=$(printf '%s' "$filter_status" | jq -sRr @uri) + query="${query}&status=eq.${encoded_status}" + fi + + if [[ -n "$filter_project" ]]; then + if [[ ! "$filter_project" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then + filter_project=$(resolve_project_id "$filter_project") + fi + query="${query}&project_id=eq.${filter_project}" + fi + + log_info "Fetching sprints..." + local response + response=$(curl -s "$query" "${HEADERS[@]}") + + if [[ "$output_json" == true ]]; then + echo "$response" | jq . + else + # Table output + local count + count=$(echo "$response" | jq 'length') + log_success "Found $count sprint(s)" + + # Print header + printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "ID" "NAME" "PROJECT" "STATUS" "START" "END" + printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "------------------------------------" "-------------------------" "------------" "----------" "------------" "------------" + + # Print rows + echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.projects.name // "N/A" | tostring | .[0:10]), .status, (.start_date // "N/A"), (.end_date // "N/A")] | @tsv' | while IFS=$'\t' read -r id name project status start end; do + printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "$id" "$name" "$project" "$status" "$start" "$end" + done + fi +} + +# Get command +cmd_get() { + local identifier="$1" + + if [[ -z "$identifier" ]]; then + log_error "Sprint ID or name required. Usage: ./sprint.sh get <id-or-name>" + exit 1 + fi + + local sprint_id + sprint_id=$(resolve_sprint_id "$identifier") + + log_info "Fetching sprint $sprint_id..." + local response + response=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}&select=*,projects(name)" \ + "${HEADERS[@]}") + + local sprint + sprint=$(echo "$response" | jq '.[0] // empty') + + if [[ -n "$sprint" && "$sprint" != "{}" && "$sprint" != "null" ]]; then + echo "$sprint" | jq . + else + log_error "Sprint not found: $identifier" + exit 1 + fi +} + +# Update command +cmd_update() { + local identifier="$1" + shift + + if [[ -z "$identifier" ]]; then + log_error "Sprint ID or name required. Usage: ./sprint.sh update <id-or-name> [options]" + exit 1 + fi + + local sprint_id + sprint_id=$(resolve_sprint_id "$identifier") + + # Build update payload + local update_fields="{}" + + while [[ $# -gt 0 ]]; do + case "${1:-}" in + --name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;; + --goal) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {goal: $v}'); shift 2 ;; + --start-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {start_date: $v}'); shift 2 ;; + --end-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {end_date: $v}'); shift 2 ;; + --status) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;; + *) shift ;; + esac + done + + # Check if we have anything to update + if [[ "$update_fields" == "{}" ]]; then + log_warning "No update fields specified" + exit 0 + fi + + # Add updated_at timestamp + local timestamp + timestamp=$(get_timestamp) + update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}') + + log_info "Updating sprint $sprint_id..." + local response + response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ + "${HEADERS[@]}" \ + -d "$update_fields") + + if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then + log_success "Sprint updated: $sprint_id" + else + log_error "Failed to update sprint" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + fi +} + +# Close command - marks sprint as completed +cmd_close() { + local identifier="$1" + + if [[ -z "$identifier" ]]; then + log_error "Sprint ID or name required. Usage: ./sprint.sh close <id-or-name>" + exit 1 + fi + + local sprint_id + sprint_id=$(resolve_sprint_id "$identifier") + + log_info "Closing sprint $sprint_id..." + local timestamp + timestamp=$(get_timestamp) + + local update_fields + update_fields=$(jq -n \ + --arg status "completed" \ + --arg closed_at "$timestamp" \ + --arg updated_at "$timestamp" \ + '{status: $status, closed_at: $closed_at, updated_at: $updated_at}') + + local response + response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ + "${HEADERS[@]}" \ + -d "$update_fields") + + if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then + log_success "Sprint closed: $sprint_id" + else + log_error "Failed to close sprint" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + fi +} + +# Delete command +cmd_delete() { + local identifier="$1" + + if [[ -z "$identifier" ]]; then + log_error "Sprint ID or name required. Usage: ./sprint.sh delete <id-or-name>" + exit 1 + fi + + local sprint_id + sprint_id=$(resolve_sprint_id "$identifier") + + log_info "Deleting sprint $sprint_id..." + local response + response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ + "${HEADERS[@]}") + + if [[ -z "$response" || "$response" == "[]" ]]; then + log_success "Sprint deleted: $sprint_id" + else + log_error "Failed to delete sprint" + echo "$response" | jq . 2>/dev/null || echo "$response" + exit 1 + fi +} + +# Main execution +check_dependencies + +# Parse command +COMMAND="${1:-}" +shift || true + +case "$COMMAND" in + create) + cmd_create "$@" + ;; + list) + cmd_list "$@" + ;; + get) + cmd_get "$@" + ;; + update) + cmd_update "$@" + ;; + close) + cmd_close "$@" + ;; + delete) + cmd_delete "$@" + ;; + help|--help|-h) + show_usage + ;; + "") + show_usage + ;; + *) + log_error "Unknown command: $COMMAND" + show_usage + exit 1 + ;; +esac diff --git a/scripts/task.sh b/scripts/task.sh new file mode 100755 index 0000000..0273cd6 --- /dev/null +++ b/scripts/task.sh @@ -0,0 +1 @@ +zsh:1: command not found: update diff --git a/src/app/page.tsx b/src/app/page.tsx index 22830fd..225ef53 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1313,11 +1313,14 @@ export default function Home() { className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500" > <option value="">Auto (Current Sprint)</option> - {sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => ( - <option key={sprint.id} value={sprint.id}> - {sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")}) - </option> - ))} + {sprints + .filter((sprint) => sprint.status !== "completed") + .sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()) + .map((sprint) => ( + <option key={sprint.id} value={sprint.id}> + {sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")}) + </option> + ))} </select> </div> </div> @@ -1530,11 +1533,14 @@ export default function Home() { className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500" > <option value="">No Sprint</option> - {sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => ( - <option key={sprint.id} value={sprint.id}> - {sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")}) - </option> - ))} + {sprints + .filter((sprint) => sprint.status !== "completed") + .sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()) + .map((sprint) => ( + <option key={sprint.id} value={sprint.id}> + {sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")}) + </option> + ))} </select> </div> diff --git a/src/app/tasks/[taskId]/page.tsx b/src/app/tasks/[taskId]/page.tsx index 8f73470..ecb9000 100644 --- a/src/app/tasks/[taskId]/page.tsx +++ b/src/app/tasks/[taskId]/page.tsx @@ -123,13 +123,13 @@ const addReplyToThread = (comments: TaskComment[], parentId: string, reply: Task if (comment.id === parentId) { return { ...comment, - replies: [...getComments(comment.replies), reply], + replies: [...(comment.replies || []), reply], } } return { ...comment, - replies: addReplyToThread(getComments(comment.replies), parentId, reply), + replies: addReplyToThread(comment.replies || [], parentId, reply), } }) @@ -138,11 +138,11 @@ const removeCommentFromThread = (comments: TaskComment[], targetId: string): Tas .filter((comment) => comment.id !== targetId) .map((comment) => ({ ...comment, - replies: removeCommentFromThread(getComments(comment.replies), targetId), + replies: removeCommentFromThread(comment.replies || [], targetId), })) const countThreadComments = (comments: TaskComment[]): number => - comments.reduce((total, comment) => total + 1 + countThreadComments(getComments(comment.replies)), 0) + comments.reduce((total, comment) => total + 1 + countThreadComments(comment.replies || []), 0) const toLabel = (raw: string) => raw.trim().replace(/^#/, "") @@ -293,7 +293,7 @@ export default function TaskDetailPage() { setEditedTask({ ...selectedTask, tags: getTags(selectedTask), - comments: getComments(selectedTask.comments), + comments: Array.isArray(selectedTask.comments) ? selectedTask.comments : [], attachments: getAttachments(selectedTask), }) setEditedTaskLabelInput("") @@ -302,7 +302,6 @@ export default function TaskDetailPage() { const editedTaskTags = editedTask ? getTags(editedTask) : [] const editedTaskAttachments = editedTask ? getAttachments(editedTask) : [] - const commentCount = editedTask ? countThreadComments(getComments(editedTask.comments)) : 0 const allLabels = useMemo(() => { const labels = new Map<string, number>() @@ -404,16 +403,13 @@ export default function TaskDetailPage() { const nextTask: Task = { ...editedTask, - comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)], + comments: [...editedTask.comments, buildComment(newComment.trim(), commentAuthorId)], } setEditedTask(nextTask) setNewComment("") - const success = await updateTask(nextTask.id, { - ...nextTask, - comments: getComments(nextTask.comments), - }) + const success = await updateTask(nextTask.id, nextTask) if (!success) { toast.error("Failed to save comment", { description: "Comment was added locally but could not sync to the server.", @@ -434,7 +430,7 @@ export default function TaskDetailPage() { } const nextTask: Task = { ...editedTask, - comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)), + comments: addReplyToThread(editedTask.comments, parentId, buildComment(text, commentAuthorId)), } setEditedTask(nextTask) @@ -442,10 +438,7 @@ export default function TaskDetailPage() { setReplyDrafts((prev) => ({ ...prev, [parentId]: "" })) setOpenReplyEditors((prev) => ({ ...prev, [parentId]: false })) - const success = await updateTask(nextTask.id, { - ...nextTask, - comments: getComments(nextTask.comments), - }) + const success = await updateTask(nextTask.id, nextTask) if (!success) { toast.error("Failed to save reply", { description: "Reply was added locally but could not sync to the server.", @@ -459,7 +452,7 @@ export default function TaskDetailPage() { setEditedTask({ ...editedTask, - comments: removeCommentFromThread(getComments(editedTask.comments), commentId), + comments: removeCommentFromThread(editedTask.comments, commentId), }) } @@ -535,10 +528,7 @@ export default function TaskDetailPage() { setSaveSuccess(false) try { - const success = await updateTask(editedTask.id, { - ...editedTask, - comments: getComments(editedTask.comments), - }) + const success = await updateTask(editedTask.id, editedTask) if (success) { setSaveSuccess(true) @@ -614,7 +604,7 @@ export default function TaskDetailPage() { const renderThread = (comments: TaskComment[], depth = 0) => comments.map((comment) => { - const replies = getComments(comment.replies) + const replies = comment.replies || [] const isReplying = !!openReplyEditors[comment.id] const replyDraft = replyDrafts[comment.id] || "" const authorId = comment.commentAuthorId @@ -1033,14 +1023,14 @@ export default function TaskDetailPage() { <div className="border-t border-slate-800 pt-6"> <h4 className="font-medium text-white mb-4 flex items-center gap-2"> <MessageSquare className="w-4 h-4" /> - Comments ({commentCount}) + Comments ({editedTask.comments.length}) </h4> <div className="space-y-3 mb-4"> - {commentCount === 0 ? ( + {editedTask.comments.length === 0 ? ( <p className="text-slate-500 text-sm">No comments yet. Add the first one.</p> ) : ( - renderThread(getComments(editedTask.comments)) + renderThread(editedTask.comments) )} </div>