diff --git a/src/api/client.ts b/src/api/client.ts index 17cac2d..5260afe 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -224,10 +224,18 @@ async function parseResponse(response: Response): Promise { if (data && typeof data === 'object') { if ('success' in data) { if (data.success === false) { + const errorMessage = typeof data.error === 'string' + ? data.error + : (data.error?.message || 'Request failed'); + + const errorDetails = typeof data.error === 'object' + ? data.error?.errors + : undefined; + throw new ApiError( - data.error?.message || 'Request failed', + errorMessage, response.status, - data.error?.errors + errorDetails ); } // If response has pagination, return object with data and pagination diff --git a/src/api/projects.ts b/src/api/projects.ts index 9b0b83b..494f282 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -130,6 +130,16 @@ export async function deleteProject(id: string): Promise { return apiClient.delete(`/api/projects/${id}`); } +/** + * Deactivate a project and unassign users + * @param id - The project ID + * @returns Promise resolving when the project is deactivated + */ +export async function deactivateProject(id: string): Promise { + const response = await apiClient.post>(`/api/projects/${id}/deactivate`, {}); + return transformKeys(response); +} + /** * Fetch unique area names from all projects * @returns Promise resolving to an array of unique area names diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx index 04f049e..097c254 100644 --- a/src/pages/projects/ProjectsPage.tsx +++ b/src/pages/projects/ProjectsPage.tsx @@ -8,6 +8,7 @@ import { createProject as apiCreateProject, updateProject as apiUpdateProject, deleteProject as apiDeleteProject, + deactivateProject as apiDeactivateProject, } from "../../api/projects"; import { fetchMeterTypes, type MeterType } from "../../api/meterTypes"; import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth"; @@ -27,6 +28,10 @@ export default function ProjectsPage() { const [meterTypes, setMeterTypes] = useState([]); + const [showDeactivateModal, setShowDeactivateModal] = useState(false); + const [projectToDeactivate, setProjectToDeactivate] = useState(null); + const [usersAssignedCount, setUsersAssignedCount] = useState(0); + const emptyForm: ProjectInput = { name: "", description: "", @@ -119,11 +124,33 @@ export default function ProjectsPage() { setActiveProject(null); } catch (error) { console.error("Error deleting project:", error); - alert( - `Error deleting project: ${ - error instanceof Error ? error.message : "Please try again." - }` - ); + + // Get error message - ApiError extends Error + let errorMessage = ""; + if (error instanceof Error) { + errorMessage = error.message; + } else if (typeof error === 'string') { + errorMessage = error; + } + + console.log("Error message:", errorMessage); // Debug log + + // Check if error is about users assigned to the project + if (errorMessage.includes("user(s) are assigned to this project")) { + // Extract number of users from error message + const match = errorMessage.match(/(\d+) user\(s\)/); + const userCount = match ? parseInt(match[1]) : 1; + + console.log("Users assigned:", userCount); // Debug log + + setUsersAssignedCount(userCount); + setProjectToDeactivate(activeProject); + setShowDeactivateModal(true); + } else { + alert( + `Error deleting project: ${errorMessage || "Please try again."}` + ); + } } }; @@ -147,6 +174,31 @@ export default function ProjectsPage() { setShowModal(true); }; + const handleConfirmDeactivate = async () => { + if (!projectToDeactivate) return; + + try { + const deactivatedProject = await apiDeactivateProject(projectToDeactivate.id); + + setProjects((prev) => + prev.map((p) => (p.id === deactivatedProject.id ? deactivatedProject : p)) + ); + + setShowDeactivateModal(false); + setProjectToDeactivate(null); + setActiveProject(null); + + alert(`Proyecto "${projectToDeactivate.name}" ha sido desactivado y los usuarios han sido desasignados.`); + } catch (error) { + console.error("Error deactivating project:", error); + alert( + `Error deactivating project: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); + } + }; + const filtered = visibleProjects.filter((p) => `${p.name} ${p.areaName} ${p.description ?? ""}` .toLowerCase() @@ -383,6 +435,60 @@ export default function ProjectsPage() { )} + + {showDeactivateModal && projectToDeactivate && ( +
+
+

+ Proyecto con Usuarios Asignados +

+ +
+

+ El proyecto "{projectToDeactivate.name}" tiene{" "} + {usersAssignedCount} usuario(s) asignado(s). +

+ +

+ No se puede eliminar directamente. ¿Deseas continuar con las siguientes acciones? +

+ +
    +
  • + + El proyecto será desactivado (status = INACTIVE) +
  • +
  • + + Los {usersAssignedCount} usuario(s) serán desasignados (project_id = null) +
  • +
+ +

+ Nota: El proyecto no será eliminado de la base de datos, solo desactivado. +

+
+ +
+ + +
+
+
+ )} ); } diff --git a/water-api/src/controllers/project.controller.ts b/water-api/src/controllers/project.controller.ts index d1c001e..100be63 100644 --- a/water-api/src/controllers/project.controller.ts +++ b/water-api/src/controllers/project.controller.ts @@ -225,3 +225,37 @@ export async function getStats(req: Request, res: Response): Promise { }); } } + +/** + * POST /projects/:id/deactivate + * Deactivate a project and unassign users + * Requires authentication + */ +export async function deactivateProject(req: AuthenticatedRequest, res: Response): Promise { + try { + const { id } = req.params; + + const project = await projectService.getById(id); + + if (!project) { + res.status(404).json({ + success: false, + error: 'Project not found', + }); + return; + } + + const deactivatedProject = await projectService.deactivateProjectAndUnassignUsers(id); + + res.status(200).json({ + success: true, + data: deactivatedProject, + }); + } catch (error) { + console.error('Error deactivating project:', error); + res.status(500).json({ + success: false, + error: 'Failed to deactivate project', + }); + } +} diff --git a/water-api/src/routes/project.routes.ts b/water-api/src/routes/project.routes.ts index a27e9db..e26e22c 100644 --- a/water-api/src/routes/project.routes.ts +++ b/water-api/src/routes/project.routes.ts @@ -54,6 +54,14 @@ router.put('/:id', authenticateToken, validateUpdateProject, projectController.u */ router.patch('/:id', authenticateToken, validateUpdateProject, projectController.update); +/** + * POST /projects/:id/deactivate + * Protected endpoint - deactivate a project and unassign users + * Headers: Authorization: Bearer + * Response: { success: true, data: Project } + */ +router.post('/:id/deactivate', authenticateToken, projectController.deactivateProject); + /** * DELETE /projects/:id * Protected endpoint - delete a project (requires admin role) diff --git a/water-api/src/services/project.service.ts b/water-api/src/services/project.service.ts index 7e4aa68..d0655f9 100644 --- a/water-api/src/services/project.service.ts +++ b/water-api/src/services/project.service.ts @@ -323,3 +323,25 @@ export async function getStats(id: string): Promise { concentrator_count: parseInt(concentratorStats.rows[0]?.count || '0', 10), }; } + +/** + * Deactivate a project and unassign all users + * @param id - Project UUID + * @returns Updated project + */ +export async function deactivateProjectAndUnassignUsers(id: string): Promise { + await query( + 'UPDATE users SET project_id = NULL WHERE project_id = $1', + [id] + ); + + const result = await query( + `UPDATE projects + SET status = 'INACTIVE', updated_at = NOW() + WHERE id = $1 + RETURNING id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at`, + [id] + ); + + return result.rows[0]; +}