chore: estructura inicial del MVP
Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias, dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
|
||||
const LIST_SELECT = `
|
||||
SELECT p.id, p.code, p.name, p.description, p.objective, p.project_type_id, pt.name AS project_type_name,
|
||||
p.priority_id, pr.name AS priority_name, pr.color AS priority_color,
|
||||
p.status_id, ps.name AS status_name, ps.color AS status_color, ps.is_closed AS status_is_closed,
|
||||
p.owner_user_id, u.name AS owner_name,
|
||||
p.requesting_department_id, rd.name AS requesting_department_name,
|
||||
p.responsible_department_id, sd.name AS responsible_department_name,
|
||||
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
|
||||
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at
|
||||
FROM projects p
|
||||
JOIN priorities pr ON pr.id = p.priority_id
|
||||
JOIN project_statuses ps ON ps.id = p.status_id
|
||||
JOIN users u ON u.id = p.owner_user_id
|
||||
LEFT JOIN project_types pt ON pt.id = p.project_type_id
|
||||
LEFT JOIN departments rd ON rd.id = p.requesting_department_id
|
||||
LEFT JOIN departments sd ON sd.id = p.responsible_department_id
|
||||
`;
|
||||
|
||||
export async function findAll({ statusId, priorityId, ownerUserId } = {}) {
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
if (statusId) { clauses.push('p.status_id = ?'); params.push(statusId); }
|
||||
if (priorityId) { clauses.push('p.priority_id = ?'); params.push(priorityId); }
|
||||
if (ownerUserId) { clauses.push('p.owner_user_id = ?'); params.push(ownerUserId); }
|
||||
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
||||
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.created_at DESC`, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function findById(id) {
|
||||
const [rows] = await pool.query(`${LIST_SELECT} WHERE p.id = ?`, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function create(data) {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO projects
|
||||
(code, name, description, objective, project_type_id, priority_id, status_id, owner_user_id,
|
||||
requesting_department_id, responsible_department_id, start_date, estimated_end_date,
|
||||
risk_level, notes, created_by)
|
||||
VALUES ('PENDING', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.name, data.description || null, data.objective || null, data.projectTypeId || null,
|
||||
data.priorityId, data.statusId, data.ownerUserId,
|
||||
data.requestingDepartmentId || null, data.responsibleDepartmentId || null,
|
||||
data.startDate || null, data.estimatedEndDate || null,
|
||||
data.riskLevel || 'low', data.notes || null, data.createdBy,
|
||||
]
|
||||
);
|
||||
const id = result.insertId;
|
||||
const code = `PRY-${String(id).padStart(4, '0')}`;
|
||||
await pool.query('UPDATE projects SET code = ? WHERE id = ?', [code, id]);
|
||||
// El creador queda como miembro por defecto.
|
||||
await pool.query('INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (?, ?, ?)', [
|
||||
id, data.ownerUserId, 'owner',
|
||||
]);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
export async function update(id, data) {
|
||||
await pool.query(
|
||||
`UPDATE projects SET
|
||||
name = ?, description = ?, objective = ?, project_type_id = ?, priority_id = ?, status_id = ?,
|
||||
owner_user_id = ?, requesting_department_id = ?, responsible_department_id = ?,
|
||||
start_date = ?, estimated_end_date = ?, actual_end_date = ?, risk_level = ?, notes = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
data.name, data.description || null, data.objective || null, data.projectTypeId || null,
|
||||
data.priorityId, data.statusId, data.ownerUserId,
|
||||
data.requestingDepartmentId || null, data.responsibleDepartmentId || null,
|
||||
data.startDate || null, data.estimatedEndDate || null, data.actualEndDate || null,
|
||||
data.riskLevel || 'low', data.notes || null, id,
|
||||
]
|
||||
);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
export async function updateProgress(id, progressPct) {
|
||||
await pool.query('UPDATE projects SET progress_pct = ? WHERE id = ?', [progressPct, id]);
|
||||
}
|
||||
|
||||
export async function listMembers(projectId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT pm.user_id, u.name, u.email, pm.role_in_project, pm.added_at
|
||||
FROM project_members pm JOIN users u ON u.id = pm.user_id
|
||||
WHERE pm.project_id = ? ORDER BY u.name`,
|
||||
[projectId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function isMember(projectId, userId) {
|
||||
const [rows] = await pool.query('SELECT 1 FROM project_members WHERE project_id = ? AND user_id = ?', [
|
||||
projectId, userId,
|
||||
]);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export async function addMember(projectId, userId, roleInProject) {
|
||||
await pool.query(
|
||||
'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (?, ?, ?)',
|
||||
[projectId, userId, roleInProject || null]
|
||||
);
|
||||
return listMembers(projectId);
|
||||
}
|
||||
|
||||
export async function removeMember(projectId, userId) {
|
||||
await pool.query('DELETE FROM project_members WHERE project_id = ? AND user_id = ?', [projectId, userId]);
|
||||
return listMembers(projectId);
|
||||
}
|
||||
|
||||
export async function averageActivityProgress(projectId) {
|
||||
const [rows] = await pool.query('SELECT AVG(progress_pct) AS avg_pct FROM activities WHERE project_id = ?', [
|
||||
projectId,
|
||||
]);
|
||||
return rows[0].avg_pct === null ? 0 : Number(rows[0].avg_pct);
|
||||
}
|
||||
Reference in New Issue
Block a user