arcadiasuite/server/production/routes.ts

857 lines
33 KiB
TypeScript

import { Router } from "express";
import { z } from "zod";
import { productionStorage } from "./storage";
import { compassStorage } from "../compass/storage";
import {
insertPcSquadSchema,
insertPcSprintSchema,
insertPcWorkItemSchema,
insertPcWorkItemCommentSchema,
insertPcTimesheetEntrySchema,
} from "@shared/schema";
const router = Router();
function requireAuth(req: any, res: any, next: any) {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication required" });
}
next();
}
async function getUserTenantId(req: any): Promise<number | null> {
const userId = req.user?.id;
if (!userId) return null;
const headerTenantId = req.headers["x-tenant-id"];
if (headerTenantId) {
const tenantId = parseInt(headerTenantId as string);
const isMember = await compassStorage.isUserInTenant(userId, tenantId);
return isMember ? tenantId : null;
}
const tenants = await compassStorage.getUserTenants(userId);
return tenants.length > 0 ? tenants[0].id : null;
}
// ========== SQUADS ==========
router.get("/squads", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const squads = await productionStorage.getSquads(tenantId ?? undefined);
res.json(squads);
} catch (error) {
res.status(500).json({ error: "Failed to fetch squads" });
}
});
router.get("/squads/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const squad = await productionStorage.getSquad(Number(req.params.id));
if (!squad) return res.status(404).json({ error: "Squad not found" });
if (squad.tenantId && squad.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
res.json(squad);
} catch (error) {
res.status(500).json({ error: "Failed to fetch squad" });
}
});
router.post("/squads", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const data = insertPcSquadSchema.parse({ ...req.body, tenantId });
const squad = await productionStorage.createSquad(data);
res.status(201).json(squad);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create squad" });
}
});
router.patch("/squads/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getSquad(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Squad not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const data = insertPcSquadSchema.partial().parse(req.body);
delete (data as any).tenantId;
const squad = await productionStorage.updateSquad(Number(req.params.id), data);
res.json(squad);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update squad" });
}
});
router.delete("/squads/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getSquad(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Squad not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
await productionStorage.deleteSquad(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete squad" });
}
});
// ========== SQUAD MEMBERS ==========
router.get("/squads/:id/members", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const squad = await productionStorage.getSquad(Number(req.params.id));
if (!squad) return res.status(404).json({ error: "Squad not found" });
if (squad.tenantId && squad.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const members = await productionStorage.getSquadMembers(Number(req.params.id));
res.json(members);
} catch (error) {
res.status(500).json({ error: "Failed to fetch squad members" });
}
});
router.post("/squads/:id/members", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const squad = await productionStorage.getSquad(Number(req.params.id));
if (!squad) return res.status(404).json({ error: "Squad not found" });
if (squad.tenantId && squad.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const member = await productionStorage.addSquadMember({
squadId: Number(req.params.id),
userId: req.body.userId || null,
collaboratorId: req.body.collaboratorId || null,
memberRole: req.body.memberRole || "member",
});
res.status(201).json(member);
} catch (error) {
res.status(500).json({ error: "Failed to add squad member" });
}
});
router.delete("/squads/:squadId/members/:userId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const squad = await productionStorage.getSquad(Number(req.params.squadId));
if (!squad) return res.status(404).json({ error: "Squad not found" });
if (squad.tenantId && squad.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const deleted = await productionStorage.removeSquadMember(Number(req.params.squadId), req.params.userId);
if (!deleted) return res.status(404).json({ error: "Member not found" });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to remove squad member" });
}
});
// ========== SPRINTS ==========
router.get("/sprints", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const projectId = req.query.projectId ? Number(req.query.projectId) : undefined;
const sprints = await productionStorage.getSprints(tenantId ?? undefined, projectId);
res.json(sprints);
} catch (error) {
res.status(500).json({ error: "Failed to fetch sprints" });
}
});
router.get("/sprints/active", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const sprint = await productionStorage.getActiveSprint(tenantId ?? undefined);
res.json(sprint || null);
} catch (error) {
res.status(500).json({ error: "Failed to fetch active sprint" });
}
});
router.get("/active-sprint", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const sprint = await productionStorage.getActiveSprint(tenantId ?? undefined);
res.json(sprint || null);
} catch (error) {
res.status(500).json({ error: "Failed to fetch active sprint" });
}
});
router.get("/my-tasks", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const user = req.user as any;
const items = await productionStorage.getMyWorkItems(user.id as string, tenantId ?? undefined);
res.json(items);
} catch (error) {
console.error("My tasks error:", error);
res.status(500).json({ error: "Failed to fetch my tasks" });
}
});
router.get("/sprints/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const sprint = await productionStorage.getSprint(Number(req.params.id));
if (!sprint) return res.status(404).json({ error: "Sprint not found" });
if (sprint.tenantId && sprint.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
res.json(sprint);
} catch (error) {
res.status(500).json({ error: "Failed to fetch sprint" });
}
});
router.post("/sprints", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const body = {
...req.body,
tenantId,
startDate: req.body.startDate ? new Date(req.body.startDate) : undefined,
endDate: req.body.endDate ? new Date(req.body.endDate) : undefined,
};
const data = insertPcSprintSchema.parse(body);
const sprint = await productionStorage.createSprint(data);
res.status(201).json(sprint);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create sprint" });
}
});
router.patch("/sprints/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getSprint(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Sprint not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const data = insertPcSprintSchema.partial().parse(req.body);
delete (data as any).tenantId;
const sprint = await productionStorage.updateSprint(Number(req.params.id), data);
res.json(sprint);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update sprint" });
}
});
router.delete("/sprints/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getSprint(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Sprint not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
await productionStorage.deleteSprint(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete sprint" });
}
});
// ========== WORK ITEMS ==========
router.get("/work-items", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const filters = {
sprintId: req.query.sprintId ? Number(req.query.sprintId) : undefined,
projectId: req.query.projectId ? Number(req.query.projectId) : undefined,
assigneeId: req.query.assigneeId as string | undefined,
status: req.query.status as string | undefined,
};
const items = await productionStorage.getWorkItems(tenantId ?? undefined, filters);
res.json(items);
} catch (error) {
console.error("Work items error:", error);
res.status(500).json({ error: "Failed to fetch work items" });
}
});
router.get("/work-items/backlog", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const projectId = req.query.projectId ? Number(req.query.projectId) : undefined;
const items = await productionStorage.getBacklogItems(tenantId ?? undefined, projectId);
res.json(items);
} catch (error) {
res.status(500).json({ error: "Failed to fetch backlog" });
}
});
router.get("/work-items/my-items", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const user = req.user as any;
const status = req.query.status as string | undefined;
const items = await productionStorage.getMyWorkItems(user.id, tenantId ?? undefined, status);
res.json(items);
} catch (error) {
res.status(500).json({ error: "Failed to fetch my work items" });
}
});
router.get("/work-items/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const item = await productionStorage.getWorkItem(Number(req.params.id));
if (!item) return res.status(404).json({ error: "Work item not found" });
if (item.tenantId && item.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
res.json(item);
} catch (error) {
res.status(500).json({ error: "Failed to fetch work item" });
}
});
router.post("/work-items", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const user = req.user as any;
const data = insertPcWorkItemSchema.parse({
...req.body,
tenantId,
createdById: user.id,
});
const item = await productionStorage.createWorkItem(data);
res.status(201).json(item);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create work item" });
}
});
router.patch("/work-items/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getWorkItem(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Work item not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const data = insertPcWorkItemSchema.partial().parse(req.body);
delete (data as any).tenantId;
const item = await productionStorage.updateWorkItem(Number(req.params.id), data);
res.json(item);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update work item" });
}
});
router.delete("/work-items/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getWorkItem(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Work item not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
await productionStorage.deleteWorkItem(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete work item" });
}
});
router.post("/work-items/:id/move-to-sprint", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getWorkItem(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Work item not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const sprintId = req.body.sprintId !== undefined ? Number(req.body.sprintId) : null;
const item = await productionStorage.moveToSprint(Number(req.params.id), sprintId);
res.json(item);
} catch (error) {
res.status(500).json({ error: "Failed to move work item" });
}
});
// ========== WORK ITEM COMMENTS ==========
router.get("/work-items/:id/comments", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const item = await productionStorage.getWorkItem(Number(req.params.id));
if (!item) return res.status(404).json({ error: "Work item not found" });
if (item.tenantId && item.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const comments = await productionStorage.getWorkItemComments(Number(req.params.id));
res.json(comments);
} catch (error) {
res.status(500).json({ error: "Failed to fetch comments" });
}
});
router.post("/work-items/:id/comments", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const item = await productionStorage.getWorkItem(Number(req.params.id));
if (!item) return res.status(404).json({ error: "Work item not found" });
if (item.tenantId && item.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const user = req.user as any;
const data = insertPcWorkItemCommentSchema.parse({
workItemId: Number(req.params.id),
userId: user.id,
content: req.body.content,
});
const comment = await productionStorage.createWorkItemComment(data);
res.status(201).json(comment);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create comment" });
}
});
// ========== TIMESHEET ==========
router.get("/timesheets", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const filters = {
userId: req.query.userId as string | undefined,
projectId: req.query.projectId ? Number(req.query.projectId) : undefined,
};
const entries = await productionStorage.getTimesheetEntries(tenantId ?? undefined, filters);
res.json(entries);
} catch (error) {
console.error("Fetch timesheet error:", error);
res.status(500).json({ error: "Failed to fetch timesheet" });
}
});
router.post("/timesheets", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const user = req.user as any;
const data = insertPcTimesheetEntrySchema.parse({
...req.body,
tenantId,
userId: user.id,
});
const entry = await productionStorage.createTimesheetEntry(data);
res.status(201).json(entry);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create timesheet entry" });
}
});
router.patch("/timesheets/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const data = insertPcTimesheetEntrySchema.partial().parse(req.body);
delete (data as any).tenantId;
const entry = await productionStorage.updateTimesheetEntry(Number(req.params.id), data);
res.json(entry);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update timesheet entry" });
}
});
router.delete("/timesheets/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
await productionStorage.deleteTimesheetEntry(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete timesheet entry" });
}
});
router.post("/timesheets/timer/start", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const user = req.user as any;
const { projectId, workItemId, hourlyRate, description } = req.body;
const entry = await productionStorage.createTimesheetEntry({
tenantId,
projectId,
workItemId,
userId: user.id,
date: new Date(),
hours: "0",
description: description || "",
hourlyRate: hourlyRate?.toString() || "0",
totalCost: "0",
status: "draft",
timerStartedAt: new Date(),
});
res.status(201).json(entry);
} catch (error) {
console.error("Start timer error:", error);
res.status(500).json({ error: "Failed to start timer" });
}
});
router.post("/timesheets/timer/stop/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
if (!existing.timerStartedAt) return res.status(400).json({ error: "Timer not started" });
const startTime = new Date(existing.timerStartedAt);
const endTime = new Date();
const hoursWorked = (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
const totalCost = hoursWorked * parseFloat(existing.hourlyRate || "0");
const entry = await productionStorage.updateTimesheetEntry(Number(req.params.id), {
hours: hoursWorked.toFixed(2),
totalCost: totalCost.toFixed(2),
timerStartedAt: null,
});
if (existing.workItemId) {
const workItem = await productionStorage.getWorkItem(existing.workItemId);
if (workItem) {
const currentHours = parseFloat(workItem.actualHours || "0");
const newHours = currentHours + hoursWorked;
const newCost = parseFloat(workItem.totalCost || "0") + totalCost;
await productionStorage.updateWorkItem(existing.workItemId, {
actualHours: newHours.toFixed(2),
totalCost: newCost.toFixed(2),
});
}
}
res.json(entry);
} catch (error) {
console.error("Stop timer error:", error);
res.status(500).json({ error: "Failed to stop timer" });
}
});
router.post("/timesheets/:id/submit", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const entry = await productionStorage.updateTimesheetEntry(Number(req.params.id), { status: "pending" });
res.json(entry);
} catch (error) {
res.status(500).json({ error: "Failed to submit for approval" });
}
});
router.post("/timesheets/:id/approve", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const user = req.user as any;
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const entry = await productionStorage.updateTimesheetEntry(Number(req.params.id), {
status: "approved",
approvedById: user.id,
approvedAt: new Date(),
});
res.json(entry);
} catch (error) {
res.status(500).json({ error: "Failed to approve" });
}
});
router.post("/timesheets/:id/reject", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const user = req.user as any;
const existing = await productionStorage.getTimesheetEntry(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Entry not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const entry = await productionStorage.updateTimesheetEntry(Number(req.params.id), {
status: "rejected",
approvedById: user.id,
approvedAt: new Date(),
});
res.json(entry);
} catch (error) {
res.status(500).json({ error: "Failed to reject" });
}
});
// ========== PROJECTS ==========
router.get("/projects", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const projects = await productionStorage.getProjects(tenantId ?? undefined);
res.json(projects);
} catch (error) {
res.status(500).json({ error: "Failed to fetch projects" });
}
});
router.post("/projects", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const { name, description, type, clientId, compassProjectId } = req.body;
let clientName = null;
if (clientId) {
const { crmStorage } = await import("../crm/storage");
const client = await crmStorage.getClient(clientId);
clientName = client?.name;
}
const user = req.user as any;
const project = await productionStorage.createProject({
tenantId,
name,
description,
type: type || "internal",
clientId: clientId || null,
clientName,
compassProjectId: compassProjectId || null,
status: "active",
userId: user?.id || "system",
});
res.status(201).json(project);
} catch (error) {
console.error("Create project error:", error);
res.status(500).json({ error: "Failed to create project" });
}
});
router.get("/projects/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const project = await productionStorage.getProject(Number(req.params.id));
if (!project) return res.status(404).json({ error: "Project not found" });
if (project.tenantId && project.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
res.json(project);
} catch (error) {
res.status(500).json({ error: "Failed to fetch project" });
}
});
router.patch("/projects/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getProject(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Project not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
const project = await productionStorage.updateProject(Number(req.params.id), req.body);
res.json(project);
} catch (error) {
res.status(500).json({ error: "Failed to update project" });
}
});
router.delete("/projects/:id", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const existing = await productionStorage.getProject(Number(req.params.id));
if (!existing) return res.status(404).json({ error: "Project not found" });
if (existing.tenantId && existing.tenantId !== tenantId) return res.status(403).json({ error: "Access denied" });
await productionStorage.deleteProject(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete project" });
}
});
// ========== STATISTICS ==========
router.get("/stats", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const user = req.user as any;
const stats = await productionStorage.getProductionStats(tenantId ?? undefined, user.id);
res.json(stats);
} catch (error) {
res.status(500).json({ error: "Failed to fetch stats" });
}
});
// ========== COLLABORATORS (Users with collaboratorType) ==========
router.get("/collaborators", requireAuth, async (req, res) => {
try {
const collaborators = await productionStorage.getCollaborators();
res.json(collaborators);
} catch (error) {
res.status(500).json({ error: "Failed to fetch collaborators" });
}
});
router.post("/collaborators", requireAuth, async (req, res) => {
try {
const { name, username, email, phone, collaboratorType, hourlyRate, skills, password } = req.body;
if (!name || !username || !collaboratorType) {
return res.status(400).json({ error: "Name, username and collaborator type are required" });
}
const collaborator = await productionStorage.createCollaborator({
name,
username,
email,
phone,
collaboratorType,
hourlyRate: hourlyRate || "0",
skills: skills || [],
password: password || "temp123",
});
res.status(201).json(collaborator);
} catch (error: any) {
console.error("Create collaborator error:", error);
res.status(500).json({ error: error.message || "Failed to create collaborator" });
}
});
router.patch("/collaborators/:id", requireAuth, async (req, res) => {
try {
const collaborator = await productionStorage.updateCollaborator(req.params.id, req.body);
if (!collaborator) return res.status(404).json({ error: "Collaborator not found" });
res.json(collaborator);
} catch (error) {
res.status(500).json({ error: "Failed to update collaborator" });
}
});
router.delete("/collaborators/:id", requireAuth, async (req, res) => {
try {
await productionStorage.deleteCollaborator(req.params.id);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete collaborator" });
}
});
// ========== PROJECT SQUAD MEMBERS ==========
router.get("/projects/:id/members", requireAuth, async (req, res) => {
try {
const members = await productionStorage.getProjectMembers(Number(req.params.id));
res.json(members);
} catch (error) {
res.status(500).json({ error: "Failed to fetch project members" });
}
});
router.post("/projects/:id/members", requireAuth, async (req, res) => {
try {
const { userId, collaboratorId, role, isExternal } = req.body;
if (!userId && !collaboratorId) {
return res.status(400).json({ error: "Either userId or collaboratorId is required" });
}
const member = await productionStorage.addProjectMember({
projectId: Number(req.params.id),
userId: userId || null,
collaboratorId: collaboratorId || null,
role: role || "member",
isExternal: isExternal || 0,
});
res.status(201).json(member);
} catch (error: any) {
console.error("Add project member error:", error);
res.status(500).json({ error: error.message || "Failed to add project member" });
}
});
router.patch("/projects/:projectId/members/:memberId", requireAuth, async (req, res) => {
try {
const { role } = req.body;
if (!role) return res.status(400).json({ error: "Role is required" });
const member = await productionStorage.updateProjectMemberRole(Number(req.params.memberId), role);
if (!member) return res.status(404).json({ error: "Member not found" });
res.json(member);
} catch (error) {
res.status(500).json({ error: "Failed to update member role" });
}
});
router.delete("/projects/:projectId/members/:memberId", requireAuth, async (req, res) => {
try {
await productionStorage.removeProjectMember(Number(req.params.memberId));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to remove project member" });
}
});
// ========== EXTERNAL COLLABORATORS ==========
router.get("/external-collaborators", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
const collaborators = await productionStorage.getExternalCollaborators(tenantId ?? undefined);
res.json(collaborators);
} catch (error) {
res.status(500).json({ error: "Failed to fetch external collaborators" });
}
});
router.post("/external-collaborators", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const { name, email, phone, type, hourlyRate, skills, notes } = req.body;
if (!name || !type) {
return res.status(400).json({ error: "Name and type are required" });
}
const collaborator = await productionStorage.createExternalCollaborator({
tenantId,
name,
email,
phone,
type,
hourlyRate: hourlyRate || "0",
skills: skills || [],
notes,
});
res.status(201).json(collaborator);
} catch (error: any) {
console.error("Create external collaborator error:", error);
res.status(500).json({ error: error.message || "Failed to create external collaborator" });
}
});
router.patch("/external-collaborators/:id", requireAuth, async (req, res) => {
try {
const collaborator = await productionStorage.updateExternalCollaborator(Number(req.params.id), req.body);
if (!collaborator) return res.status(404).json({ error: "External collaborator not found" });
res.json(collaborator);
} catch (error) {
res.status(500).json({ error: "Failed to update external collaborator" });
}
});
router.delete("/external-collaborators/:id", requireAuth, async (req, res) => {
try {
await productionStorage.deleteExternalCollaborator(Number(req.params.id));
res.status(204).send();
} catch (error) {
res.status(500).json({ error: "Failed to delete external collaborator" });
}
});
// ========== TENANT PRODUCTION SETTINGS ==========
router.get("/settings", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const settings = await productionStorage.getTenantSettings(tenantId);
res.json(settings || { tenantId, timesheetRequiresApproval: 0, timesheetAllowTimer: 1, defaultHourlyRate: "0", workHoursPerDay: "8" });
} catch (error) {
res.status(500).json({ error: "Failed to fetch settings" });
}
});
router.patch("/settings", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const settings = await productionStorage.upsertTenantSettings(tenantId, req.body);
res.json(settings);
} catch (error) {
res.status(500).json({ error: "Failed to update settings" });
}
});
export default router;