feat: el buscador de Proyectos y Actividades tambien busca en tareas
Buscador de Proyectos: ahora busca tambien en actividades y tareas de cualquier proyecto (antes solo nombre/codigo del proyecto). Resultados en un panel flotante con la ruta completa "Proyecto - Actividad - Tarea"; clic navega al proyecto y abre la actividad encontrada (pestaña Tareas si el resultado era una tarea). Buscador de Actividades (dentro de un proyecto): busca tambien en las tareas de esa actividad, mismo panel con "Actividad - Tarea". Nuevo GET /api/search?q=...&projectId= (global o acotado a un proyecto). Nuevos componentes/hooks reutilizables: SearchResultsDropdown, useDebouncedValue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -19,6 +19,7 @@ import auditRoutes from './modules/audit/routes.js';
|
||||
import settingsRoutes from './modules/settings/routes.js';
|
||||
import companyRoutes from './modules/company/routes.js';
|
||||
import recurringTasksRoutes from './modules/recurring-tasks/routes.js';
|
||||
import searchRoutes from './modules/search/routes.js';
|
||||
|
||||
export const app = express();
|
||||
|
||||
@@ -42,6 +43,7 @@ app.use('/api/audit', auditRoutes);
|
||||
app.use('/api/settings', settingsRoutes);
|
||||
app.use('/api/company', companyRoutes);
|
||||
app.use('/api/recurring-tasks', recurringTasksRoutes);
|
||||
app.use('/api/search', searchRoutes);
|
||||
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
|
||||
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
|
||||
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ok, asyncHandler } from '../../utils/http.js';
|
||||
import * as service from './service.js';
|
||||
|
||||
export const getSearch = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.search(req.query.q, req.query.projectId));
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
|
||||
const RESULT_LIMIT = 20;
|
||||
|
||||
// Busqueda global (menu Proyectos): proyectos, actividades (con su proyecto) y tareas (con su
|
||||
// actividad y proyecto) — para armar la migaja de pan "Proyecto - Actividad - Tarea" en el cliente.
|
||||
export async function searchGlobal(term) {
|
||||
const like = `%${term}%`;
|
||||
|
||||
const [projects] = await pool.query(
|
||||
`SELECT id, code, name FROM projects
|
||||
WHERE deleted_at IS NULL AND (code LIKE ? OR name LIKE ?)
|
||||
ORDER BY name LIMIT ?`,
|
||||
[like, like, RESULT_LIMIT]
|
||||
);
|
||||
|
||||
const [activities] = await pool.query(
|
||||
`SELECT a.id, a.code, a.name,
|
||||
p.id AS project_id, p.code AS project_code, p.name AS project_name
|
||||
FROM activities a
|
||||
JOIN projects p ON p.id = a.project_id
|
||||
WHERE p.deleted_at IS NULL AND (a.code LIKE ? OR a.name LIKE ?)
|
||||
ORDER BY a.name LIMIT ?`,
|
||||
[like, like, RESULT_LIMIT]
|
||||
);
|
||||
|
||||
const [tasks] = await pool.query(
|
||||
`SELECT t.id, t.name,
|
||||
a.id AS activity_id, a.code AS activity_code, a.name AS activity_name,
|
||||
p.id AS project_id, p.code AS project_code, p.name AS project_name
|
||||
FROM tasks t
|
||||
JOIN activities a ON a.id = t.activity_id
|
||||
JOIN projects p ON p.id = a.project_id
|
||||
WHERE p.deleted_at IS NULL AND t.name LIKE ?
|
||||
ORDER BY t.name LIMIT ?`,
|
||||
[like, RESULT_LIMIT]
|
||||
);
|
||||
|
||||
return { projects, activities, tasks };
|
||||
}
|
||||
|
||||
// Busqueda dentro de un proyecto (pestaña Actividades): solo actividades y tareas de ESE
|
||||
// proyecto — la migaja de pan ahi es "Actividad - Tarea" (el proyecto ya es el contexto visible).
|
||||
export async function searchInProject(projectId, term) {
|
||||
const like = `%${term}%`;
|
||||
|
||||
const [activities] = await pool.query(
|
||||
`SELECT id, code, name FROM activities
|
||||
WHERE project_id = ? AND (code LIKE ? OR name LIKE ?)
|
||||
ORDER BY name LIMIT ?`,
|
||||
[projectId, like, like, RESULT_LIMIT]
|
||||
);
|
||||
|
||||
const [tasks] = await pool.query(
|
||||
`SELECT t.id, t.name, a.id AS activity_id, a.code AS activity_code, a.name AS activity_name
|
||||
FROM tasks t
|
||||
JOIN activities a ON a.id = t.activity_id
|
||||
WHERE a.project_id = ? AND t.name LIKE ?
|
||||
ORDER BY t.name LIMIT ?`,
|
||||
[projectId, like, RESULT_LIMIT]
|
||||
);
|
||||
|
||||
return { activities, tasks };
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { getSearch } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.get('/', getSearch);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as repo from './repository.js';
|
||||
|
||||
const MIN_LENGTH = 2;
|
||||
|
||||
export async function search(term, projectId) {
|
||||
const q = (term || '').trim();
|
||||
if (q.length < MIN_LENGTH) {
|
||||
return projectId ? { activities: [], tasks: [] } : { projects: [], activities: [], tasks: [] };
|
||||
}
|
||||
return projectId ? repo.searchInProject(Number(projectId), q) : repo.searchGlobal(q);
|
||||
}
|
||||
Reference in New Issue
Block a user