Add complete CRUD APIs for projects and sprints
- New API endpoints: - GET /api/projects - List projects - POST /api/projects - Create project - PATCH /api/projects - Update project - DELETE /api/projects - Delete project - GET /api/sprints - List sprints - POST /api/sprints - Create sprint - PATCH /api/sprints - Update sprint - DELETE /api/sprints - Delete sprint - Added CLI commands: - project create/update/delete - sprint create/update/delete - Updated help text with new commands Web UI can now do full CRUD on projects/sprints and CLI matches 1:1
This commit is contained in:
parent
b354a0469d
commit
42188457da
135
scripts/gantt.sh
135
scripts/gantt.sh
@ -292,9 +292,54 @@ cmd_task_attach() {
|
|||||||
|
|
||||||
cmd_project_list() {
|
cmd_project_list() {
|
||||||
log_info "Fetching projects..."
|
log_info "Fetching projects..."
|
||||||
local response
|
api_call GET "/projects" | jq '.projects'
|
||||||
response=$(api_call GET "/tasks")
|
}
|
||||||
echo "$response" | jq '.projects'
|
|
||||||
|
cmd_project_create() {
|
||||||
|
local name="$1"
|
||||||
|
local description="${2:-}"
|
||||||
|
local color="${3:-#3b82f6}"
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
log_error "Usage: project create <name> [description] [color]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating project: $name"
|
||||||
|
local data
|
||||||
|
data=$(jq -n --arg name "$name" --arg desc "$description" --arg color "$color" \
|
||||||
|
'{name: $name, description: (if $desc == "" then null else $desc end), color: $color}')
|
||||||
|
api_call POST "/projects" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_project_update() {
|
||||||
|
local project_id="$1"
|
||||||
|
local field="$2"
|
||||||
|
local value="$3"
|
||||||
|
|
||||||
|
if [ -z "$project_id" ] || [ -z "$field" ]; then
|
||||||
|
log_error "Usage: project update <id> <field> <value>"
|
||||||
|
echo "Fields: name, description, color"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating project $project_id: $field = $value"
|
||||||
|
local data
|
||||||
|
data=$(jq -n --arg id "$project_id" --arg field "$field" --arg value "$value" \
|
||||||
|
'{id: $id, ($field): $value}')
|
||||||
|
api_call PATCH "/projects" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_project_delete() {
|
||||||
|
local project_id="$1"
|
||||||
|
|
||||||
|
if [ -z "$project_id" ]; then
|
||||||
|
log_error "Usage: project delete <id>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_warn "Deleting project $project_id..."
|
||||||
|
api_call DELETE "/projects" "{\"id\": \"$project_id\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
#===================
|
#===================
|
||||||
@ -303,9 +348,68 @@ cmd_project_list() {
|
|||||||
|
|
||||||
cmd_sprint_list() {
|
cmd_sprint_list() {
|
||||||
log_info "Fetching sprints..."
|
log_info "Fetching sprints..."
|
||||||
local response
|
api_call GET "/sprints" | jq '.sprints'
|
||||||
response=$(api_call GET "/tasks")
|
}
|
||||||
echo "$response" | jq '.sprints'
|
|
||||||
|
cmd_sprint_create() {
|
||||||
|
local name="$1"
|
||||||
|
local project_id="$2"
|
||||||
|
local start_date="${3:-}"
|
||||||
|
local end_date="${4:-}"
|
||||||
|
local goal="${5:-}"
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
log_error "Usage: sprint create <name> <project-id> [start-date] [end-date] [goal]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating sprint: $name"
|
||||||
|
local data
|
||||||
|
data=$(jq -n \
|
||||||
|
--arg name "$name" \
|
||||||
|
--arg projectId "$project_id" \
|
||||||
|
--arg startDate "$start_date" \
|
||||||
|
--arg endDate "$end_date" \
|
||||||
|
--arg goal "$goal" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
projectId: $projectId,
|
||||||
|
startDate: (if $startDate == "" then null else $startDate end),
|
||||||
|
endDate: (if $endDate == "" then null else $endDate end),
|
||||||
|
goal: (if $goal == "" then null else $goal end),
|
||||||
|
status: "planning"
|
||||||
|
}')
|
||||||
|
api_call POST "/sprints" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sprint_update() {
|
||||||
|
local sprint_id="$1"
|
||||||
|
local field="$2"
|
||||||
|
local value="$3"
|
||||||
|
|
||||||
|
if [ -z "$sprint_id" ] || [ -z "$field" ]; then
|
||||||
|
log_error "Usage: sprint update <id> <field> <value>"
|
||||||
|
echo "Fields: name, goal, startDate, endDate, status, projectId"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating sprint $sprint_id: $field = $value"
|
||||||
|
local data
|
||||||
|
data=$(jq -n --arg id "$sprint_id" --arg field "$field" --arg value "$value" \
|
||||||
|
'{id: $id, ($field): $value}')
|
||||||
|
api_call PATCH "/sprints" "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sprint_delete() {
|
||||||
|
local sprint_id="$1"
|
||||||
|
|
||||||
|
if [ -z "$sprint_id" ]; then
|
||||||
|
log_error "Usage: sprint delete <id>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_warn "Deleting sprint $sprint_id..."
|
||||||
|
api_call DELETE "/sprints" "{\"id\": \"$sprint_id\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
#===================
|
#===================
|
||||||
@ -434,9 +538,22 @@ TASK COMMANDS:
|
|||||||
|
|
||||||
PROJECT COMMANDS:
|
PROJECT COMMANDS:
|
||||||
project list List all projects
|
project list List all projects
|
||||||
|
project create <name> [desc] [color]
|
||||||
|
Create new project
|
||||||
|
project update <id> <field> <val>
|
||||||
|
Update project field
|
||||||
|
Fields: name, description, color
|
||||||
|
project delete <id> Delete a project
|
||||||
|
|
||||||
SPRINT COMMANDS:
|
SPRINT COMMANDS:
|
||||||
sprint list List all sprints
|
sprint list List all sprints
|
||||||
|
sprint create <name> <project-id> [start] [end] [goal]
|
||||||
|
Create new sprint
|
||||||
|
sprint update <id> <field> <val>
|
||||||
|
Update sprint field
|
||||||
|
Fields: name, goal, startDate, endDate,
|
||||||
|
status, projectId
|
||||||
|
sprint delete <id> Delete a sprint
|
||||||
|
|
||||||
AUTH COMMANDS:
|
AUTH COMMANDS:
|
||||||
auth login <email> <pass> Log in
|
auth login <email> <pass> Log in
|
||||||
@ -505,6 +622,9 @@ main() {
|
|||||||
shift || true
|
shift || true
|
||||||
case "$subcmd" in
|
case "$subcmd" in
|
||||||
list|ls) cmd_project_list "$@" ;;
|
list|ls) cmd_project_list "$@" ;;
|
||||||
|
create|new|add) cmd_project_create "$@" ;;
|
||||||
|
update|set|edit) cmd_project_update "$@" ;;
|
||||||
|
delete|rm|remove) cmd_project_delete "$@" ;;
|
||||||
*) log_error "Unknown project command: $subcmd"; show_help; exit 1 ;;
|
*) log_error "Unknown project command: $subcmd"; show_help; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@ -513,6 +633,9 @@ main() {
|
|||||||
shift || true
|
shift || true
|
||||||
case "$subcmd" in
|
case "$subcmd" in
|
||||||
list|ls) cmd_sprint_list "$@" ;;
|
list|ls) cmd_sprint_list "$@" ;;
|
||||||
|
create|new|add) cmd_sprint_create "$@" ;;
|
||||||
|
update|set|edit) cmd_sprint_update "$@" ;;
|
||||||
|
delete|rm|remove) cmd_sprint_delete "$@" ;;
|
||||||
*) log_error "Unknown sprint command: $subcmd"; show_help; exit 1 ;;
|
*) log_error "Unknown sprint command: $subcmd"; show_help; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|||||||
129
src/app/api/projects/route.ts
Normal file
129
src/app/api/projects/route.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||||
|
import { getAuthenticatedUser } from "@/lib/server/auth";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
// GET - fetch all projects
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { data: projects, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ projects: projects || [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API GET /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch projects" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - create a new project
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, description, color } = body;
|
||||||
|
|
||||||
|
if (!name || typeof name !== "string") {
|
||||||
|
return NextResponse.json({ error: "Missing project name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
description: description || null,
|
||||||
|
color: color || "#3b82f6",
|
||||||
|
created_at: now,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, project: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API POST /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to create project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH - update a project
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, ...updates } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing project id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.update({
|
||||||
|
...updates,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
.eq("id", id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, project: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API PATCH /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to update project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - delete a project
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await request.json();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing project id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { error } = await supabase.from("projects").delete().eq("id", id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API DELETE /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to delete project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/app/api/sprints/route.ts
Normal file
139
src/app/api/sprints/route.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||||
|
import { getAuthenticatedUser } from "@/lib/server/auth";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
// GET - fetch all sprints
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { data: sprints, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.select("*")
|
||||||
|
.order("start_date", { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ sprints: sprints || [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API GET /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch sprints" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - create a new sprint
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, goal, startDate, endDate, status, projectId } = body;
|
||||||
|
|
||||||
|
if (!name || typeof name !== "string") {
|
||||||
|
return NextResponse.json({ error: "Missing sprint name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
goal: goal || null,
|
||||||
|
start_date: startDate || now,
|
||||||
|
end_date: endDate || now,
|
||||||
|
status: status || "planning",
|
||||||
|
project_id: projectId || null,
|
||||||
|
created_at: now,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, sprint: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API POST /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to create sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH - update a sprint
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, ...updates } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing sprint id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Map camelCase to snake_case for database
|
||||||
|
const dbUpdates: Record<string, unknown> = {};
|
||||||
|
if (updates.name !== undefined) dbUpdates.name = updates.name;
|
||||||
|
if (updates.goal !== undefined) dbUpdates.goal = updates.goal;
|
||||||
|
if (updates.startDate !== undefined) dbUpdates.start_date = updates.startDate;
|
||||||
|
if (updates.endDate !== undefined) dbUpdates.end_date = updates.endDate;
|
||||||
|
if (updates.status !== undefined) dbUpdates.status = updates.status;
|
||||||
|
if (updates.projectId !== undefined) dbUpdates.project_id = updates.projectId;
|
||||||
|
dbUpdates.updated_at = now;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.update(dbUpdates)
|
||||||
|
.eq("id", id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, sprint: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API PATCH /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to update sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - delete a sprint
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await request.json();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing sprint id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { error } = await supabase.from("sprints").delete().eq("id", id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API DELETE /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to delete sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user