import { query, queryOne } from '../../config/database.js'; import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; export interface Project { id: string; tenant_id: string; company_id: string; company_name?: string; name: string; code?: string; description?: string; manager_id?: string; manager_name?: string; partner_id?: string; partner_name?: string; analytic_account_id?: string; date_start?: Date; date_end?: Date; status: 'draft' | 'active' | 'completed' | 'cancelled' | 'on_hold'; privacy: 'public' | 'private' | 'followers'; allow_timesheets: boolean; color?: string; task_count?: number; completed_task_count?: number; created_at: Date; } export interface CreateProjectDto { company_id: string; name: string; code?: string; description?: string; manager_id?: string; partner_id?: string; date_start?: string; date_end?: string; privacy?: 'public' | 'private' | 'followers'; allow_timesheets?: boolean; color?: string; } export interface UpdateProjectDto { name?: string; code?: string | null; description?: string | null; manager_id?: string | null; partner_id?: string | null; date_start?: string | null; date_end?: string | null; status?: 'draft' | 'active' | 'completed' | 'cancelled' | 'on_hold'; privacy?: 'public' | 'private' | 'followers'; allow_timesheets?: boolean; color?: string | null; } export interface ProjectFilters { company_id?: string; manager_id?: string; partner_id?: string; status?: string; search?: string; page?: number; limit?: number; } class ProjectsService { async findAll(tenantId: string, filters: ProjectFilters = {}): Promise<{ data: Project[]; total: number }> { const { company_id, manager_id, partner_id, status, search, page = 1, limit = 20 } = filters; const offset = (page - 1) * limit; let whereClause = 'WHERE p.tenant_id = $1 AND p.deleted_at IS NULL'; const params: any[] = [tenantId]; let paramIndex = 2; if (company_id) { whereClause += ` AND p.company_id = $${paramIndex++}`; params.push(company_id); } if (manager_id) { whereClause += ` AND p.manager_id = $${paramIndex++}`; params.push(manager_id); } if (partner_id) { whereClause += ` AND p.partner_id = $${paramIndex++}`; params.push(partner_id); } if (status) { whereClause += ` AND p.status = $${paramIndex++}`; params.push(status); } if (search) { whereClause += ` AND (p.name ILIKE $${paramIndex} OR p.code ILIKE $${paramIndex})`; params.push(`%${search}%`); paramIndex++; } const countResult = await queryOne<{ count: string }>( `SELECT COUNT(*) as count FROM projects.projects p ${whereClause}`, params ); params.push(limit, offset); const data = await query( `SELECT p.*, c.name as company_name, u.name as manager_name, pr.name as partner_name, (SELECT COUNT(*) FROM projects.tasks t WHERE t.project_id = p.id AND t.deleted_at IS NULL) as task_count, (SELECT COUNT(*) FROM projects.tasks t WHERE t.project_id = p.id AND t.status = 'done' AND t.deleted_at IS NULL) as completed_task_count FROM projects.projects p LEFT JOIN auth.companies c ON p.company_id = c.id LEFT JOIN auth.users u ON p.manager_id = u.id LEFT JOIN core.partners pr ON p.partner_id = pr.id ${whereClause} ORDER BY p.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, params ); return { data, total: parseInt(countResult?.count || '0', 10), }; } async findById(id: string, tenantId: string): Promise { const project = await queryOne( `SELECT p.*, c.name as company_name, u.name as manager_name, pr.name as partner_name, (SELECT COUNT(*) FROM projects.tasks t WHERE t.project_id = p.id AND t.deleted_at IS NULL) as task_count, (SELECT COUNT(*) FROM projects.tasks t WHERE t.project_id = p.id AND t.status = 'done' AND t.deleted_at IS NULL) as completed_task_count FROM projects.projects p LEFT JOIN auth.companies c ON p.company_id = c.id LEFT JOIN auth.users u ON p.manager_id = u.id LEFT JOIN core.partners pr ON p.partner_id = pr.id WHERE p.id = $1 AND p.tenant_id = $2 AND p.deleted_at IS NULL`, [id, tenantId] ); if (!project) { throw new NotFoundError('Proyecto no encontrado'); } return project; } async create(dto: CreateProjectDto, tenantId: string, userId: string): Promise { // Check unique code if provided if (dto.code) { const existing = await queryOne( `SELECT id FROM projects.projects WHERE company_id = $1 AND code = $2 AND deleted_at IS NULL`, [dto.company_id, dto.code] ); if (existing) { throw new ConflictError('Ya existe un proyecto con ese código'); } } const project = await queryOne( `INSERT INTO projects.projects ( tenant_id, company_id, name, code, description, manager_id, partner_id, date_start, date_end, privacy, allow_timesheets, color, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *`, [ tenantId, dto.company_id, dto.name, dto.code, dto.description, dto.manager_id, dto.partner_id, dto.date_start, dto.date_end, dto.privacy || 'public', dto.allow_timesheets ?? true, dto.color, userId ] ); return project!; } async update(id: string, dto: UpdateProjectDto, tenantId: string, userId: string): Promise { const existing = await this.findById(id, tenantId); const updateFields: string[] = []; const values: any[] = []; let paramIndex = 1; if (dto.name !== undefined) { updateFields.push(`name = $${paramIndex++}`); values.push(dto.name); } if (dto.code !== undefined) { if (dto.code) { const existingCode = await queryOne( `SELECT id FROM projects.projects WHERE company_id = $1 AND code = $2 AND id != $3 AND deleted_at IS NULL`, [existing.company_id, dto.code, id] ); if (existingCode) { throw new ConflictError('Ya existe un proyecto con ese código'); } } updateFields.push(`code = $${paramIndex++}`); values.push(dto.code); } if (dto.description !== undefined) { updateFields.push(`description = $${paramIndex++}`); values.push(dto.description); } if (dto.manager_id !== undefined) { updateFields.push(`manager_id = $${paramIndex++}`); values.push(dto.manager_id); } if (dto.partner_id !== undefined) { updateFields.push(`partner_id = $${paramIndex++}`); values.push(dto.partner_id); } if (dto.date_start !== undefined) { updateFields.push(`date_start = $${paramIndex++}`); values.push(dto.date_start); } if (dto.date_end !== undefined) { updateFields.push(`date_end = $${paramIndex++}`); values.push(dto.date_end); } if (dto.status !== undefined) { updateFields.push(`status = $${paramIndex++}`); values.push(dto.status); } if (dto.privacy !== undefined) { updateFields.push(`privacy = $${paramIndex++}`); values.push(dto.privacy); } if (dto.allow_timesheets !== undefined) { updateFields.push(`allow_timesheets = $${paramIndex++}`); values.push(dto.allow_timesheets); } if (dto.color !== undefined) { updateFields.push(`color = $${paramIndex++}`); values.push(dto.color); } if (updateFields.length === 0) { return existing; } updateFields.push(`updated_by = $${paramIndex++}`); values.push(userId); updateFields.push(`updated_at = CURRENT_TIMESTAMP`); values.push(id, tenantId); await query( `UPDATE projects.projects SET ${updateFields.join(', ')} WHERE id = $${paramIndex++} AND tenant_id = $${paramIndex} AND deleted_at IS NULL`, values ); return this.findById(id, tenantId); } async delete(id: string, tenantId: string, userId: string): Promise { await this.findById(id, tenantId); // Soft delete await query( `UPDATE projects.projects SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 WHERE id = $2 AND tenant_id = $3`, [userId, id, tenantId] ); } async getStats(id: string, tenantId: string): Promise { await this.findById(id, tenantId); const stats = await queryOne<{ total_tasks: number; completed_tasks: number; in_progress_tasks: number; total_hours: number; total_milestones: number; completed_milestones: number; }>( `SELECT (SELECT COUNT(*) FROM projects.tasks WHERE project_id = $1 AND deleted_at IS NULL) as total_tasks, (SELECT COUNT(*) FROM projects.tasks WHERE project_id = $1 AND status = 'done' AND deleted_at IS NULL) as completed_tasks, (SELECT COUNT(*) FROM projects.tasks WHERE project_id = $1 AND status = 'in_progress' AND deleted_at IS NULL) as in_progress_tasks, (SELECT COALESCE(SUM(hours), 0) FROM projects.timesheets WHERE project_id = $1) as total_hours, (SELECT COUNT(*) FROM projects.milestones WHERE project_id = $1) as total_milestones, (SELECT COUNT(*) FROM projects.milestones WHERE project_id = $1 AND status = 'completed') as completed_milestones`, [id] ); return { total_tasks: parseInt(String(stats?.total_tasks || 0)), completed_tasks: parseInt(String(stats?.completed_tasks || 0)), in_progress_tasks: parseInt(String(stats?.in_progress_tasks || 0)), completion_percentage: stats?.total_tasks ? Math.round((parseInt(String(stats.completed_tasks)) / parseInt(String(stats.total_tasks))) * 100) : 0, total_hours: parseFloat(String(stats?.total_hours || 0)), total_milestones: parseInt(String(stats?.total_milestones || 0)), completed_milestones: parseInt(String(stats?.completed_milestones || 0)), }; } } export const projectsService = new ProjectsService();