arcadia-suite-sv/server/production/storage.ts

511 lines
19 KiB
TypeScript

import { db } from "../../db/index";
import { eq, and, or, desc, sql, isNull, isNotNull } from "drizzle-orm";
import {
pcSquads,
pcSquadMembers,
pcSprints,
pcWorkItems,
pcWorkItemComments,
pcTimesheetEntries,
pcProjects,
pcProjectMembers,
pcCollaborators,
tenantProductionSettings,
users,
type PcSquad,
type InsertPcSquad,
type PcSquadMember,
type InsertPcSquadMember,
type PcSprint,
type InsertPcSprint,
type PcWorkItem,
type InsertPcWorkItem,
type PcWorkItemComment,
type InsertPcWorkItemComment,
type PcTimesheetEntry,
type InsertPcTimesheetEntry,
type PcProject,
type InsertPcProject,
type PcProjectMember,
type InsertPcProjectMember,
type PcCollaborator,
type InsertPcCollaborator,
type TenantProductionSettings,
type InsertTenantProductionSettings,
} from "@shared/schema";
import { scrypt, randomBytes } from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(scrypt);
async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16).toString("hex");
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
return `${buf.toString("hex")}.${salt}`;
}
class ProductionStorage {
// ========== SQUADS ==========
async getSquads(tenantId?: number): Promise<PcSquad[]> {
if (tenantId) {
return db.select().from(pcSquads).where(eq(pcSquads.tenantId, tenantId)).orderBy(desc(pcSquads.createdAt));
}
return db.select().from(pcSquads).orderBy(desc(pcSquads.createdAt));
}
async getSquad(id: number): Promise<PcSquad | undefined> {
const [squad] = await db.select().from(pcSquads).where(eq(pcSquads.id, id));
return squad;
}
async createSquad(data: InsertPcSquad): Promise<PcSquad> {
const [squad] = await db.insert(pcSquads).values(data).returning();
return squad;
}
async updateSquad(id: number, data: Partial<InsertPcSquad>): Promise<PcSquad | undefined> {
const [squad] = await db.update(pcSquads).set({ ...data, updatedAt: new Date() }).where(eq(pcSquads.id, id)).returning();
return squad;
}
async deleteSquad(id: number): Promise<boolean> {
const result = await db.delete(pcSquads).where(eq(pcSquads.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== SQUAD MEMBERS ==========
async getSquadMembers(squadId: number): Promise<(PcSquadMember & { userName?: string; collaboratorType?: string | null; hourlyRate?: string | null })[]> {
const results = await db
.select({
id: pcSquadMembers.id,
squadId: pcSquadMembers.squadId,
userId: pcSquadMembers.userId,
memberRole: pcSquadMembers.memberRole,
joinedAt: pcSquadMembers.joinedAt,
userName: users.name,
collaboratorType: users.collaboratorType,
hourlyRate: users.hourlyRate,
})
.from(pcSquadMembers)
.leftJoin(users, eq(pcSquadMembers.userId, users.id))
.where(eq(pcSquadMembers.squadId, squadId));
return results as any;
}
async addSquadMember(data: InsertPcSquadMember): Promise<PcSquadMember> {
const [member] = await db.insert(pcSquadMembers).values(data).returning();
return member;
}
async removeSquadMember(squadId: number, userId: string): Promise<boolean> {
const result = await db.delete(pcSquadMembers).where(
and(eq(pcSquadMembers.squadId, squadId), eq(pcSquadMembers.userId, userId))
);
return (result.rowCount ?? 0) > 0;
}
// ========== SPRINTS ==========
async getSprints(tenantId?: number, projectId?: number): Promise<(PcSprint & { projectName?: string })[]> {
const conditions = [];
if (tenantId) conditions.push(eq(pcSprints.tenantId, tenantId));
if (projectId) conditions.push(eq(pcSprints.projectId, projectId));
const results = await db
.select({
id: pcSprints.id,
tenantId: pcSprints.tenantId,
projectId: pcSprints.projectId,
squadId: pcSprints.squadId,
name: pcSprints.name,
goal: pcSprints.goal,
startDate: pcSprints.startDate,
endDate: pcSprints.endDate,
status: pcSprints.status,
velocity: pcSprints.velocity,
completedPoints: pcSprints.completedPoints,
createdAt: pcSprints.createdAt,
updatedAt: pcSprints.updatedAt,
projectName: pcProjects.name,
})
.from(pcSprints)
.leftJoin(pcProjects, eq(pcSprints.projectId, pcProjects.id))
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(pcSprints.startDate), desc(pcSprints.createdAt));
return results as any;
}
async getActiveSprint(tenantId?: number): Promise<PcSprint | undefined> {
const conditions = [eq(pcSprints.status, "active")];
if (tenantId) conditions.push(eq(pcSprints.tenantId, tenantId));
const [sprint] = await db.select().from(pcSprints).where(and(...conditions)).limit(1);
return sprint;
}
async getSprint(id: number): Promise<PcSprint | undefined> {
const [sprint] = await db.select().from(pcSprints).where(eq(pcSprints.id, id));
return sprint;
}
async createSprint(data: InsertPcSprint): Promise<PcSprint> {
const [sprint] = await db.insert(pcSprints).values(data).returning();
return sprint;
}
async updateSprint(id: number, data: Partial<InsertPcSprint>): Promise<PcSprint | undefined> {
const [sprint] = await db.update(pcSprints).set({ ...data, updatedAt: new Date() }).where(eq(pcSprints.id, id)).returning();
return sprint;
}
async deleteSprint(id: number): Promise<boolean> {
const result = await db.delete(pcSprints).where(eq(pcSprints.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== WORK ITEMS ==========
async getWorkItems(tenantId?: number, filters?: { sprintId?: number; projectId?: number; assigneeId?: string; status?: string }): Promise<PcWorkItem[]> {
const conditions = [];
if (tenantId) conditions.push(eq(pcWorkItems.tenantId, tenantId));
if (filters?.sprintId) conditions.push(eq(pcWorkItems.sprintId, filters.sprintId));
if (filters?.projectId) conditions.push(eq(pcWorkItems.projectId, filters.projectId));
if (filters?.assigneeId) conditions.push(eq(pcWorkItems.assigneeId, filters.assigneeId));
if (filters?.status) conditions.push(eq(pcWorkItems.status, filters.status));
if (conditions.length > 0) {
return db.select().from(pcWorkItems).where(and(...conditions)).orderBy(desc(pcWorkItems.createdAt));
}
return db.select().from(pcWorkItems).orderBy(desc(pcWorkItems.createdAt));
}
async getBacklogItems(tenantId?: number, projectId?: number): Promise<PcWorkItem[]> {
const conditions = [isNull(pcWorkItems.sprintId)];
if (tenantId) conditions.push(eq(pcWorkItems.tenantId, tenantId));
if (projectId) conditions.push(eq(pcWorkItems.projectId, projectId));
return db.select().from(pcWorkItems).where(and(...conditions)).orderBy(desc(pcWorkItems.createdAt));
}
async getMyWorkItems(userId: string, tenantId?: number, status?: string): Promise<PcWorkItem[]> {
const conditions = [eq(pcWorkItems.assigneeId, userId)];
if (tenantId) conditions.push(eq(pcWorkItems.tenantId, tenantId));
if (status) conditions.push(eq(pcWorkItems.status, status));
return db.select().from(pcWorkItems).where(and(...conditions)).orderBy(desc(pcWorkItems.createdAt));
}
async getWorkItem(id: number): Promise<PcWorkItem | undefined> {
const [item] = await db.select().from(pcWorkItems).where(eq(pcWorkItems.id, id));
return item;
}
async createWorkItem(data: InsertPcWorkItem): Promise<PcWorkItem> {
const [item] = await db.insert(pcWorkItems).values(data).returning();
return item;
}
async updateWorkItem(id: number, data: Partial<InsertPcWorkItem>): Promise<PcWorkItem | undefined> {
const updateData: any = { ...data, updatedAt: new Date() };
if (data.status === "done" && !data.completedAt) {
updateData.completedAt = new Date();
}
const [item] = await db.update(pcWorkItems).set(updateData).where(eq(pcWorkItems.id, id)).returning();
return item;
}
async deleteWorkItem(id: number): Promise<boolean> {
const result = await db.delete(pcWorkItems).where(eq(pcWorkItems.id, id));
return (result.rowCount ?? 0) > 0;
}
async moveToSprint(workItemId: number, sprintId: number | null): Promise<PcWorkItem | undefined> {
return this.updateWorkItem(workItemId, { sprintId });
}
// ========== WORK ITEM COMMENTS ==========
async getWorkItemComments(workItemId: number): Promise<PcWorkItemComment[]> {
return db.select().from(pcWorkItemComments).where(eq(pcWorkItemComments.workItemId, workItemId)).orderBy(pcWorkItemComments.createdAt);
}
async createWorkItemComment(data: InsertPcWorkItemComment): Promise<PcWorkItemComment> {
const [comment] = await db.insert(pcWorkItemComments).values(data).returning();
return comment;
}
async deleteWorkItemComment(id: number): Promise<boolean> {
const result = await db.delete(pcWorkItemComments).where(eq(pcWorkItemComments.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== TIMESHEET ==========
async getTimesheetEntries(tenantId?: number, filters?: { userId?: string; projectId?: number; startDate?: Date; endDate?: Date }): Promise<PcTimesheetEntry[]> {
const conditions = [];
if (tenantId) conditions.push(eq(pcTimesheetEntries.tenantId, tenantId));
if (filters?.userId) conditions.push(eq(pcTimesheetEntries.userId, filters.userId));
if (filters?.projectId) conditions.push(eq(pcTimesheetEntries.projectId, filters.projectId));
if (conditions.length > 0) {
return db.select().from(pcTimesheetEntries).where(and(...conditions)).orderBy(desc(pcTimesheetEntries.date));
}
return db.select().from(pcTimesheetEntries).orderBy(desc(pcTimesheetEntries.date));
}
async getTimesheetEntry(id: number): Promise<PcTimesheetEntry | undefined> {
const [entry] = await db.select().from(pcTimesheetEntries).where(eq(pcTimesheetEntries.id, id));
return entry;
}
async createTimesheetEntry(data: InsertPcTimesheetEntry): Promise<PcTimesheetEntry> {
const [entry] = await db.insert(pcTimesheetEntries).values(data).returning();
return entry;
}
async updateTimesheetEntry(id: number, data: Partial<InsertPcTimesheetEntry>): Promise<PcTimesheetEntry | undefined> {
const [entry] = await db.update(pcTimesheetEntries).set(data).where(eq(pcTimesheetEntries.id, id)).returning();
return entry;
}
async deleteTimesheetEntry(id: number): Promise<boolean> {
const result = await db.delete(pcTimesheetEntries).where(eq(pcTimesheetEntries.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== STATISTICS ==========
async getProductionStats(tenantId?: number, userId?: string): Promise<any> {
const conditions = [];
if (tenantId) conditions.push(eq(pcWorkItems.tenantId, tenantId));
if (userId) conditions.push(eq(pcWorkItems.assigneeId, userId));
const allItems = await this.getWorkItems(tenantId, userId ? { assigneeId: userId } : undefined);
const activeSprint = await this.getActiveSprint(tenantId);
const stats = {
totalItems: allItems.length,
backlog: allItems.filter(i => i.status === "backlog").length,
inProgress: allItems.filter(i => i.status === "in_progress").length,
review: allItems.filter(i => i.status === "review").length,
done: allItems.filter(i => i.status === "done").length,
activeSprint: activeSprint ? {
id: activeSprint.id,
name: activeSprint.name,
endDate: activeSprint.endDate,
} : null,
};
return stats;
}
// ========== PROJECTS ==========
async getProjects(tenantId?: number): Promise<any[]> {
const conditions = [];
if (tenantId) conditions.push(eq(pcProjects.tenantId, tenantId));
conditions.push(eq(pcProjects.projectType, "programacao"));
const projects = conditions.length > 0
? await db.select().from(pcProjects).where(and(...conditions)).orderBy(desc(pcProjects.createdAt))
: await db.select().from(pcProjects).orderBy(desc(pcProjects.createdAt));
return projects.map(p => ({
...p,
type: p.prodType || (p.clientId ? "external" : "internal"),
}));
}
async getProject(id: number): Promise<PcProject | undefined> {
const [project] = await db.select().from(pcProjects).where(eq(pcProjects.id, id));
return project;
}
async createProject(data: any): Promise<PcProject> {
const projectData = {
tenantId: data.tenantId,
name: data.name,
description: data.description,
projectType: "programacao",
prodType: data.type || "internal",
clientId: data.clientId || null,
clientName: data.clientName || null,
compassProjectId: data.compassProjectId || null,
status: data.status || "active",
userId: data.userId || "system",
};
const [project] = await db.insert(pcProjects).values(projectData).returning();
return project;
}
async updateProject(id: number, data: Partial<InsertPcProject>): Promise<PcProject | undefined> {
const [project] = await db.update(pcProjects)
.set({ ...data, updatedAt: new Date() })
.where(eq(pcProjects.id, id))
.returning();
return project;
}
async deleteProject(id: number): Promise<boolean> {
const result = await db.delete(pcProjects).where(eq(pcProjects.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== COLLABORATORS ==========
async getCollaborators(): Promise<any[]> {
const result = await db.select({
id: users.id,
name: users.name,
username: users.username,
email: users.email,
phone: users.phone,
collaboratorType: users.collaboratorType,
hourlyRate: users.hourlyRate,
skills: users.skills,
status: users.status,
createdAt: users.createdAt,
}).from(users).where(isNotNull(users.collaboratorType)).orderBy(desc(users.createdAt));
return result;
}
async createCollaborator(data: {
name: string;
username: string;
email?: string;
phone?: string;
collaboratorType: string;
hourlyRate?: string;
skills?: string[];
password: string;
}): Promise<any> {
const hashedPassword = await hashPassword(data.password);
const [user] = await db.insert(users).values({
username: data.username,
password: hashedPassword,
name: data.name,
email: data.email,
phone: data.phone,
collaboratorType: data.collaboratorType,
hourlyRate: data.hourlyRate || "0",
skills: data.skills || [],
role: "user",
status: "active",
}).returning();
const { password, ...collaborator } = user;
return collaborator;
}
async updateCollaborator(id: string, data: Partial<{
name: string;
email: string;
phone: string;
collaboratorType: string;
hourlyRate: string;
skills: string[];
status: string;
}>): Promise<any | undefined> {
const [user] = await db.update(users).set(data).where(eq(users.id, id)).returning();
if (!user) return undefined;
const { password, ...collaborator } = user;
return collaborator;
}
async deleteCollaborator(id: string): Promise<boolean> {
const result = await db.delete(users).where(eq(users.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== PROJECT SQUAD MEMBERS ==========
async getProjectMembers(projectId: number): Promise<any[]> {
const results = await db
.select({
id: pcProjectMembers.id,
projectId: pcProjectMembers.projectId,
userId: pcProjectMembers.userId,
collaboratorId: pcProjectMembers.collaboratorId,
role: pcProjectMembers.role,
isExternal: pcProjectMembers.isExternal,
assignedAt: pcProjectMembers.assignedAt,
userName: users.name,
userEmail: users.email,
userHourlyRate: users.hourlyRate,
})
.from(pcProjectMembers)
.leftJoin(users, eq(pcProjectMembers.userId, users.id))
.where(eq(pcProjectMembers.projectId, projectId));
// For external collaborators, fetch their info from pcCollaborators
const enrichedResults = await Promise.all(results.map(async (member) => {
if (member.isExternal === 1 && member.collaboratorId) {
const [collaborator] = await db.select().from(pcCollaborators).where(eq(pcCollaborators.id, member.collaboratorId));
return {
...member,
collaboratorName: collaborator?.name,
collaboratorEmail: collaborator?.email,
collaboratorHourlyRate: collaborator?.hourlyRate,
collaboratorType: collaborator?.type,
};
}
return member;
}));
return enrichedResults;
}
async addProjectMember(data: InsertPcProjectMember): Promise<PcProjectMember> {
const [member] = await db.insert(pcProjectMembers).values(data).returning();
return member;
}
async updateProjectMemberRole(id: number, role: string): Promise<PcProjectMember | undefined> {
const [member] = await db.update(pcProjectMembers).set({ role }).where(eq(pcProjectMembers.id, id)).returning();
return member;
}
async removeProjectMember(id: number): Promise<boolean> {
const result = await db.delete(pcProjectMembers).where(eq(pcProjectMembers.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== EXTERNAL COLLABORATORS ==========
async getExternalCollaborators(tenantId?: number): Promise<PcCollaborator[]> {
if (tenantId) {
return db.select().from(pcCollaborators).where(eq(pcCollaborators.tenantId, tenantId)).orderBy(desc(pcCollaborators.createdAt));
}
return db.select().from(pcCollaborators).orderBy(desc(pcCollaborators.createdAt));
}
async getExternalCollaborator(id: number): Promise<PcCollaborator | undefined> {
const [collaborator] = await db.select().from(pcCollaborators).where(eq(pcCollaborators.id, id));
return collaborator;
}
async createExternalCollaborator(data: InsertPcCollaborator): Promise<PcCollaborator> {
const [collaborator] = await db.insert(pcCollaborators).values(data).returning();
return collaborator;
}
async updateExternalCollaborator(id: number, data: Partial<InsertPcCollaborator>): Promise<PcCollaborator | undefined> {
const [collaborator] = await db.update(pcCollaborators).set({ ...data, updatedAt: new Date() }).where(eq(pcCollaborators.id, id)).returning();
return collaborator;
}
async deleteExternalCollaborator(id: number): Promise<boolean> {
const result = await db.delete(pcCollaborators).where(eq(pcCollaborators.id, id));
return (result.rowCount ?? 0) > 0;
}
// ========== TENANT PRODUCTION SETTINGS ==========
async getTenantSettings(tenantId: number): Promise<TenantProductionSettings | undefined> {
const [settings] = await db.select().from(tenantProductionSettings).where(eq(tenantProductionSettings.tenantId, tenantId));
return settings;
}
async upsertTenantSettings(tenantId: number, data: Partial<InsertTenantProductionSettings>): Promise<TenantProductionSettings> {
const existing = await this.getTenantSettings(tenantId);
if (existing) {
const [settings] = await db.update(tenantProductionSettings).set({ ...data, updatedAt: new Date() }).where(eq(tenantProductionSettings.tenantId, tenantId)).returning();
return settings;
}
const [settings] = await db.insert(tenantProductionSettings).values({ tenantId, ...data }).returning();
return settings;
}
}
export const productionStorage = new ProductionStorage();