Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"resume": "Resume",
"personality": "Personality",
"leaderboard": "Leaderboard",
"kanban": "Kanban Board",
"home": "Home",
"features": "Features",
"openMenu": "Open navigation menu",
Expand Down
1 change: 1 addition & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"resume": "CV",
"personality": "Personalidad",
"leaderboard": "Clasificación",
"kanban": "Tablero Kanban",
"home": "Inicio",
"features": "Funciones",
"openMenu": "Abrir menú de navegación",
Expand Down
68 changes: 68 additions & 0 deletions src/app/api/kanban/[projectId]/activity/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function GET(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

// Parse URL query filters
const { searchParams } = new URL(req.url);
const actionType = searchParams.get("actionType");
const dateFrom = searchParams.get("dateFrom");
const dateTo = searchParams.get("dateTo");
const limit = parseInt(searchParams.get("limit") || "20", 10);
const offset = parseInt(searchParams.get("offset") || "0", 10);

let query = supabaseAdmin
.from("activity_log")
.select("*")
.eq("project_id", projectId)
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1);

if (actionType) {
query = query.eq("action", actionType);
}
if (dateFrom) {
query = query.gte("created_at", dateFrom);
}
if (dateTo) {
query = query.lte("created_at", dateTo);
}

const { data: activities, error: activitiesError } = await query;

if (activitiesError) {
return Response.json({ error: activitiesError.message }, { status: 500 });
}

return Response.json({ activities });
}
93 changes: 93 additions & 0 deletions src/app/api/kanban/[projectId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function GET(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

// Get stages
const { data: stages, error: stagesError } = await supabaseAdmin
.from("workflow_stages")
.select("*")
.eq("project_id", projectId)
.order("position", { ascending: true });

if (stagesError) {
return Response.json({ error: stagesError.message }, { status: 500 });
}

// Get tasks
const { data: tasks, error: tasksError } = await supabaseAdmin
.from("tasks")
.select("*")
.eq("project_id", projectId)
.order("position", { ascending: true });

if (tasksError) {
return Response.json({ error: tasksError.message }, { status: 500 });
}

// Get dependencies
const { data: dependencies, error: depsError } = await supabaseAdmin
.from("task_dependencies")
.select("*")
.eq("project_id", projectId);

if (depsError) {
return Response.json({ error: depsError.message }, { status: 500 });
}

return Response.json({ project, stages, tasks, dependencies });
}

export async function DELETE(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Delete project (cascade will delete stages and tasks)
const { error } = await supabaseAdmin
.from("projects")
.delete()
.eq("id", projectId)
.eq("user_id", user.id);

if (error) {
return Response.json({ error: error.message }, { status: 500 });
}

return Response.json({ success: true });
}
163 changes: 163 additions & 0 deletions src/app/api/kanban/[projectId]/stages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { supabaseAdmin } from "@/lib/supabase";
import { resolveAppUser } from "@/lib/resolve-user";

export const dynamic = "force-dynamic";

interface RouteParams {
params: Promise<{
projectId: string;
}>;
}

export async function POST(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { name, color, position } = body;
if (!name) {
return Response.json({ error: "Stage name is required" }, { status: 400 });
}
if (typeof position !== "number") {
return Response.json({ error: "Position must be a number" }, { status: 400 });
}

const { data: stage, error: stageError } = await supabaseAdmin
.from("workflow_stages")
.insert({
project_id: projectId,
name,
color: color || "#6366f1",
position,
})
.select("*")
.single();

if (stageError) {
return Response.json({ error: stageError.message }, { status: 500 });
}

// Log activity
await supabaseAdmin.from("activity_log").insert({
project_id: projectId,
user_id: user.id,
action: "stage_created",
entity_type: "stage",
entity_id: stage.id,
metadata: { name: stage.name },
});

return Response.json({ stage }, { status: 201 });
}

export async function PUT(req: Request, props: RouteParams) {
const { projectId } = await props.params;
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

// Verify project belongs to user
const { data: project, error: projectError } = await supabaseAdmin
.from("projects")
.select("*")
.eq("id", projectId)
.eq("user_id", user.id)
.single();

if (projectError || !project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}

let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}

const { stages, deleteStageId } = body;

if (deleteStageId) {
// Delete the stage
const { error: deleteError } = await supabaseAdmin
.from("workflow_stages")
.delete()
.eq("id", deleteStageId)
.eq("project_id", projectId);

if (deleteError) {
return Response.json({ error: deleteError.message }, { status: 500 });
}

// Log activity
await supabaseAdmin.from("activity_log").insert({
project_id: projectId,
user_id: user.id,
action: "stage_deleted",
entity_type: "stage",
entity_id: deleteStageId,
metadata: { id: deleteStageId },
});
}

if (stages && Array.isArray(stages)) {
// Bulk upsert/update positions and attributes of stages
const payload = stages.map((s) => ({
id: s.id,
project_id: projectId,
name: s.name,
color: s.color || "#6366f1",
position: s.position,
}));

const { error: upsertError } = await supabaseAdmin
.from("workflow_stages")
.upsert(payload, { onConflict: "id" });

if (upsertError) {
return Response.json({ error: upsertError.message }, { status: 500 });
}

// Log activity
await supabaseAdmin.from("activity_log").insert({
project_id: projectId,
user_id: user.id,
action: "stages_reordered",
entity_type: "stage",
entity_id: projectId,
metadata: { count: stages.length },
});
}

return Response.json({ success: true });
}
Loading
Loading