857 lines
33 KiB
TypeScript
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;
|