mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
- ModuleManager: Central tracking for installed marketplace modules - DataAnalyzerWidget: Real-time CPU/RAM/Battery/Storage widget (unlocked by Data Analyzer module) - BottomNavBar: Navigation bar for Projects/Chat/Marketplace/Settings - RootShell: Real root command execution utility - TerminalActivity: Full root shell with neofetch, sysinfo, real Linux commands - Terminal Pro module: Adds aliases (ll, la, h), command history - ArcadeActivity + SnakeGame: Pixel Arcade module unlocks retro games - fade_in/fade_out animations for smooth transitions
926 lines
25 KiB
TypeScript
926 lines
25 KiB
TypeScript
/**
|
|
* GameForge Routes
|
|
* API endpoints for game development project management
|
|
* Ported from aethex-forge
|
|
*/
|
|
|
|
import { Router, Request, Response } from "express";
|
|
import { supabase } from "./supabase.js";
|
|
import { requireAuth } from "./auth.js";
|
|
|
|
const router = Router();
|
|
|
|
// Helper to get user ID from session
|
|
function getUserId(req: Request): string | null {
|
|
return (req.session as any)?.userId || null;
|
|
}
|
|
|
|
// ==================== PROJECTS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/projects - List all projects
|
|
* GET /api/gameforge/projects?id=xxx - Get single project
|
|
*/
|
|
router.get("/projects", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { id, status, platform, limit = 50, offset = 0 } = req.query;
|
|
|
|
if (id) {
|
|
// Get single project with full details
|
|
const { data, error } = await supabase
|
|
.from("gameforge_projects")
|
|
.select(`
|
|
*,
|
|
gameforge_team_members(*)
|
|
`)
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
if (!data) return res.status(404).json({ error: "Project not found" });
|
|
|
|
return res.json(data);
|
|
}
|
|
|
|
// List all projects with filters
|
|
let query = supabase
|
|
.from("gameforge_projects")
|
|
.select(`
|
|
id,
|
|
name,
|
|
description,
|
|
status,
|
|
platform,
|
|
genre,
|
|
target_release_date,
|
|
actual_release_date,
|
|
team_size,
|
|
budget,
|
|
current_spend,
|
|
lead_id,
|
|
created_at
|
|
`, { count: "exact" });
|
|
|
|
if (status) query = query.eq("status", status as string);
|
|
if (platform) query = query.eq("platform", platform as string);
|
|
|
|
const { data, error, count } = await query
|
|
.order("created_at", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
projects: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Projects] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/projects - Create a new project
|
|
*/
|
|
router.post("/projects", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
name,
|
|
description,
|
|
platform,
|
|
genre,
|
|
target_release_date,
|
|
budget,
|
|
repository_url,
|
|
documentation_url
|
|
} = req.body;
|
|
|
|
if (!name || !platform) {
|
|
return res.status(400).json({ error: "name and platform are required" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_projects")
|
|
.insert({
|
|
name,
|
|
description,
|
|
status: "planning",
|
|
lead_id: userId,
|
|
platform,
|
|
genre: genre || [],
|
|
target_release_date,
|
|
budget,
|
|
repository_url,
|
|
documentation_url
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Projects] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/gameforge/projects/:id - Update a project
|
|
*/
|
|
router.put("/projects/:id", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { id } = req.params;
|
|
const {
|
|
name,
|
|
description,
|
|
status,
|
|
platform,
|
|
genre,
|
|
target_release_date,
|
|
actual_release_date,
|
|
budget,
|
|
current_spend,
|
|
repository_url,
|
|
documentation_url
|
|
} = req.body;
|
|
|
|
// Verify user is project lead
|
|
const { data: project, error: checkError } = await supabase
|
|
.from("gameforge_projects")
|
|
.select("lead_id")
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if (checkError || !project) {
|
|
return res.status(404).json({ error: "Project not found" });
|
|
}
|
|
|
|
if (project.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can update" });
|
|
}
|
|
|
|
const updateData: any = {};
|
|
if (name !== undefined) updateData.name = name;
|
|
if (description !== undefined) updateData.description = description;
|
|
if (status !== undefined) updateData.status = status;
|
|
if (platform !== undefined) updateData.platform = platform;
|
|
if (genre !== undefined) updateData.genre = genre;
|
|
if (target_release_date !== undefined) updateData.target_release_date = target_release_date;
|
|
if (actual_release_date !== undefined) updateData.actual_release_date = actual_release_date;
|
|
if (budget !== undefined) updateData.budget = budget;
|
|
if (current_spend !== undefined) updateData.current_spend = current_spend;
|
|
if (repository_url !== undefined) updateData.repository_url = repository_url;
|
|
if (documentation_url !== undefined) updateData.documentation_url = documentation_url;
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_projects")
|
|
.update(updateData)
|
|
.eq("id", id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Projects] Update error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== TEAM MEMBERS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/team - List team members
|
|
*/
|
|
router.get("/team", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { user_id, project_id, role, limit = 50, offset = 0 } = req.query;
|
|
|
|
let query = supabase
|
|
.from("gameforge_team_members")
|
|
.select("*", { count: "exact" });
|
|
|
|
if (user_id) {
|
|
const { data, error } = await supabase
|
|
.from("gameforge_team_members")
|
|
.select("*")
|
|
.eq("user_id", user_id)
|
|
.single();
|
|
|
|
if (error && error.code !== "PGRST116") throw error;
|
|
return res.json({ member: data });
|
|
}
|
|
|
|
if (project_id) query = query.contains("project_ids", [project_id]);
|
|
if (role) query = query.eq("role", role as string);
|
|
query = query.eq("is_active", true);
|
|
|
|
const { data, error, count } = await query
|
|
.order("joined_date", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
members: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Team] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/team - Add team member
|
|
*/
|
|
router.post("/team", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
user_id,
|
|
role,
|
|
position,
|
|
contract_type,
|
|
hourly_rate,
|
|
skills,
|
|
bio,
|
|
project_ids
|
|
} = req.body;
|
|
|
|
if (!user_id || !role) {
|
|
return res.status(400).json({ error: "user_id and role are required" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_team_members")
|
|
.insert({
|
|
user_id,
|
|
role,
|
|
position,
|
|
contract_type: contract_type || "contractor",
|
|
hourly_rate,
|
|
skills: skills || [],
|
|
bio,
|
|
project_ids: project_ids || [],
|
|
is_active: true
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Team] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/gameforge/team/:id - Update team member
|
|
*/
|
|
router.put("/team/:id", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { id } = req.params;
|
|
const {
|
|
role,
|
|
position,
|
|
contract_type,
|
|
hourly_rate,
|
|
skills,
|
|
bio,
|
|
project_ids,
|
|
is_active
|
|
} = req.body;
|
|
|
|
const updateData: any = {};
|
|
if (role !== undefined) updateData.role = role;
|
|
if (position !== undefined) updateData.position = position;
|
|
if (contract_type !== undefined) updateData.contract_type = contract_type;
|
|
if (hourly_rate !== undefined) updateData.hourly_rate = hourly_rate;
|
|
if (skills !== undefined) updateData.skills = skills;
|
|
if (bio !== undefined) updateData.bio = bio;
|
|
if (project_ids !== undefined) updateData.project_ids = project_ids;
|
|
if (is_active !== undefined) {
|
|
updateData.is_active = is_active;
|
|
if (is_active === false) updateData.left_date = new Date().toISOString();
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_team_members")
|
|
.update(updateData)
|
|
.eq("id", id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Team] Update error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== BUILDS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/builds - List builds for a project
|
|
*/
|
|
router.get("/builds", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { id, project_id, build_type, limit = 50, offset = 0 } = req.query;
|
|
|
|
if (id) {
|
|
const { data, error } = await supabase
|
|
.from("gameforge_builds")
|
|
.select(`
|
|
*,
|
|
gameforge_projects(id, name, platform)
|
|
`)
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
if (!data) return res.status(404).json({ error: "Build not found" });
|
|
|
|
return res.json(data);
|
|
}
|
|
|
|
if (!project_id) {
|
|
return res.status(400).json({ error: "project_id is required" });
|
|
}
|
|
|
|
let query = supabase
|
|
.from("gameforge_builds")
|
|
.select(`
|
|
*,
|
|
gameforge_projects(id, name)
|
|
`, { count: "exact" })
|
|
.eq("project_id", project_id);
|
|
|
|
if (build_type) query = query.eq("build_type", build_type as string);
|
|
|
|
const { data, error, count } = await query
|
|
.order("release_date", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
builds: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Builds] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/builds - Create a new build
|
|
*/
|
|
router.post("/builds", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
project_id,
|
|
version,
|
|
build_type,
|
|
download_url,
|
|
changelog,
|
|
file_size,
|
|
target_platforms
|
|
} = req.body;
|
|
|
|
if (!project_id || !version || !build_type) {
|
|
return res.status(400).json({ error: "project_id, version, and build_type are required" });
|
|
}
|
|
|
|
// Verify user is project lead
|
|
const { data: project } = await supabase
|
|
.from("gameforge_projects")
|
|
.select("lead_id")
|
|
.eq("id", project_id)
|
|
.single();
|
|
|
|
if (project?.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can create builds" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_builds")
|
|
.insert({
|
|
project_id,
|
|
version,
|
|
build_type,
|
|
download_url,
|
|
changelog,
|
|
file_size,
|
|
target_platforms: target_platforms || [],
|
|
created_by: userId
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Builds] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/gameforge/builds/:id - Update a build
|
|
*/
|
|
router.put("/builds/:id", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { id } = req.params;
|
|
const { version, build_type, download_url, changelog, file_size } = req.body;
|
|
|
|
// Verify user is project lead
|
|
const { data: build } = await supabase
|
|
.from("gameforge_builds")
|
|
.select("project_id, gameforge_projects(lead_id)")
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if ((build?.gameforge_projects as any)?.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can update builds" });
|
|
}
|
|
|
|
const updateData: any = {};
|
|
if (version !== undefined) updateData.version = version;
|
|
if (build_type !== undefined) updateData.build_type = build_type;
|
|
if (download_url !== undefined) updateData.download_url = download_url;
|
|
if (changelog !== undefined) updateData.changelog = changelog;
|
|
if (file_size !== undefined) updateData.file_size = file_size;
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_builds")
|
|
.update(updateData)
|
|
.eq("id", id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Builds] Update error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== SPRINTS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/sprints - List sprints for a project
|
|
*/
|
|
router.get("/sprints", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { project_id, phase, status, limit = 50, offset = 0 } = req.query;
|
|
|
|
if (!project_id) {
|
|
return res.status(400).json({ error: "project_id is required" });
|
|
}
|
|
|
|
let query = supabase
|
|
.from("gameforge_sprints")
|
|
.select("*", { count: "exact" })
|
|
.eq("project_id", project_id);
|
|
|
|
if (phase) query = query.eq("phase", phase as string);
|
|
if (status) query = query.eq("status", status as string);
|
|
|
|
const { data, error, count } = await query
|
|
.order("sprint_number", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
sprints: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Sprints] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/sprints - Create a new sprint
|
|
*/
|
|
router.post("/sprints", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
project_id,
|
|
sprint_number,
|
|
title,
|
|
description,
|
|
goal,
|
|
start_date,
|
|
end_date,
|
|
planned_velocity
|
|
} = req.body;
|
|
|
|
if (!project_id || !sprint_number || !title) {
|
|
return res.status(400).json({ error: "project_id, sprint_number, and title are required" });
|
|
}
|
|
|
|
// Verify user is project lead
|
|
const { data: project } = await supabase
|
|
.from("gameforge_projects")
|
|
.select("lead_id")
|
|
.eq("id", project_id)
|
|
.single();
|
|
|
|
if (project?.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can create sprints" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_sprints")
|
|
.insert({
|
|
project_id,
|
|
sprint_number,
|
|
title,
|
|
description,
|
|
goal,
|
|
start_date,
|
|
end_date,
|
|
planned_velocity,
|
|
created_by: userId
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Sprints] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/gameforge/sprints/:id - Update a sprint
|
|
*/
|
|
router.put("/sprints/:id", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { id } = req.params;
|
|
const {
|
|
title,
|
|
description,
|
|
phase,
|
|
status,
|
|
goal,
|
|
start_date,
|
|
end_date,
|
|
planned_velocity,
|
|
actual_velocity
|
|
} = req.body;
|
|
|
|
// Verify user is project lead
|
|
const { data: sprint } = await supabase
|
|
.from("gameforge_sprints")
|
|
.select("project_id, gameforge_projects(lead_id)")
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if ((sprint?.gameforge_projects as any)?.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can update sprints" });
|
|
}
|
|
|
|
const updateData: any = {};
|
|
if (title !== undefined) updateData.title = title;
|
|
if (description !== undefined) updateData.description = description;
|
|
if (phase !== undefined) updateData.phase = phase;
|
|
if (status !== undefined) updateData.status = status;
|
|
if (goal !== undefined) updateData.goal = goal;
|
|
if (start_date !== undefined) updateData.start_date = start_date;
|
|
if (end_date !== undefined) updateData.end_date = end_date;
|
|
if (planned_velocity !== undefined) updateData.planned_velocity = planned_velocity;
|
|
if (actual_velocity !== undefined) updateData.actual_velocity = actual_velocity;
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_sprints")
|
|
.update(updateData)
|
|
.eq("id", id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Sprints] Update error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== TASKS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/tasks - List tasks
|
|
*/
|
|
router.get("/tasks", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { sprint_id, project_id, status, assigned_to, limit = 100, offset = 0 } = req.query;
|
|
|
|
let query = supabase
|
|
.from("gameforge_tasks")
|
|
.select("*", { count: "exact" });
|
|
|
|
if (sprint_id) query = query.eq("sprint_id", sprint_id);
|
|
if (project_id) query = query.eq("project_id", project_id);
|
|
if (status) query = query.eq("status", status as string);
|
|
if (assigned_to) query = query.eq("assigned_to", assigned_to);
|
|
|
|
const { data, error, count } = await query
|
|
.order("created_at", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
tasks: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Tasks] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/tasks - Create a task
|
|
*/
|
|
router.post("/tasks", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
sprint_id,
|
|
project_id,
|
|
title,
|
|
description,
|
|
priority,
|
|
estimated_hours,
|
|
assigned_to,
|
|
due_date
|
|
} = req.body;
|
|
|
|
if (!project_id || !title) {
|
|
return res.status(400).json({ error: "project_id and title are required" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_tasks")
|
|
.insert({
|
|
sprint_id: sprint_id || null,
|
|
project_id,
|
|
title,
|
|
description,
|
|
priority: priority || "medium",
|
|
estimated_hours,
|
|
assigned_to: assigned_to || null,
|
|
created_by: userId,
|
|
due_date: due_date || null,
|
|
status: "todo"
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Tasks] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/gameforge/tasks/:id - Update a task
|
|
*/
|
|
router.put("/tasks/:id", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { id } = req.params;
|
|
const {
|
|
title,
|
|
description,
|
|
status,
|
|
priority,
|
|
estimated_hours,
|
|
actual_hours,
|
|
assigned_to,
|
|
due_date
|
|
} = req.body;
|
|
|
|
// Verify user can edit (task creator, assignee, or project lead)
|
|
const { data: task, error: taskError } = await supabase
|
|
.from("gameforge_tasks")
|
|
.select("project_id, assigned_to, created_by")
|
|
.eq("id", id)
|
|
.single();
|
|
|
|
if (taskError || !task) {
|
|
return res.status(404).json({ error: "Task not found" });
|
|
}
|
|
|
|
const { data: project } = await supabase
|
|
.from("gameforge_projects")
|
|
.select("lead_id")
|
|
.eq("id", task.project_id)
|
|
.single();
|
|
|
|
if (
|
|
task.assigned_to !== userId &&
|
|
task.created_by !== userId &&
|
|
project?.lead_id !== userId
|
|
) {
|
|
return res.status(403).json({ error: "No permission to edit task" });
|
|
}
|
|
|
|
const updateData: any = {};
|
|
if (title !== undefined) updateData.title = title;
|
|
if (description !== undefined) updateData.description = description;
|
|
if (status !== undefined) {
|
|
updateData.status = status;
|
|
if (status === "done") updateData.completed_at = new Date().toISOString();
|
|
else updateData.completed_at = null;
|
|
}
|
|
if (priority !== undefined) updateData.priority = priority;
|
|
if (estimated_hours !== undefined) updateData.estimated_hours = estimated_hours;
|
|
if (actual_hours !== undefined) updateData.actual_hours = actual_hours;
|
|
if (assigned_to !== undefined) updateData.assigned_to = assigned_to;
|
|
if (due_date !== undefined) updateData.due_date = due_date;
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_tasks")
|
|
.update(updateData)
|
|
.eq("id", id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Tasks] Update error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== METRICS ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/gameforge/metrics - Get project metrics
|
|
*/
|
|
router.get("/metrics", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { project_id, metric_type, limit = 50, offset = 0 } = req.query;
|
|
|
|
if (!project_id) {
|
|
return res.status(400).json({ error: "project_id is required" });
|
|
}
|
|
|
|
let query = supabase
|
|
.from("gameforge_metrics")
|
|
.select("*", { count: "exact" })
|
|
.eq("project_id", project_id);
|
|
|
|
if (metric_type) query = query.eq("metric_type", metric_type as string);
|
|
|
|
const { data, error, count } = await query
|
|
.order("metric_date", { ascending: false })
|
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({
|
|
metrics: data || [],
|
|
total: count || 0,
|
|
limit: Number(limit),
|
|
offset: Number(offset)
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[GameForge Metrics] List error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/gameforge/metrics - Record project metrics
|
|
*/
|
|
router.post("/metrics", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const {
|
|
project_id,
|
|
metric_type,
|
|
metric_date,
|
|
velocity,
|
|
hours_logged,
|
|
team_size_avg,
|
|
bugs_found,
|
|
bugs_fixed,
|
|
build_count,
|
|
days_from_planned_to_release,
|
|
on_schedule,
|
|
budget_allocated,
|
|
budget_spent
|
|
} = req.body;
|
|
|
|
if (!project_id || !metric_type) {
|
|
return res.status(400).json({ error: "project_id and metric_type are required" });
|
|
}
|
|
|
|
// Verify user is project lead
|
|
const { data: project } = await supabase
|
|
.from("gameforge_projects")
|
|
.select("lead_id")
|
|
.eq("id", project_id)
|
|
.single();
|
|
|
|
if (project?.lead_id !== userId) {
|
|
return res.status(403).json({ error: "Only project lead can record metrics" });
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("gameforge_metrics")
|
|
.insert({
|
|
project_id,
|
|
metric_type,
|
|
metric_date: metric_date || new Date().toISOString(),
|
|
velocity,
|
|
hours_logged,
|
|
team_size_avg,
|
|
bugs_found,
|
|
bugs_fixed,
|
|
build_count,
|
|
days_from_planned_to_release,
|
|
on_schedule,
|
|
budget_allocated,
|
|
budget_spent
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
res.status(201).json(data);
|
|
} catch (err: any) {
|
|
console.error("[GameForge Metrics] Create error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|