Project deleting
This commit is contained in:
@@ -224,10 +224,18 @@ async function parseResponse<T>(response: Response): Promise<T> {
|
|||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
if ('success' in data) {
|
if ('success' in data) {
|
||||||
if (data.success === false) {
|
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(
|
throw new ApiError(
|
||||||
data.error?.message || 'Request failed',
|
errorMessage,
|
||||||
response.status,
|
response.status,
|
||||||
data.error?.errors
|
errorDetails
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// If response has pagination, return object with data and pagination
|
// If response has pagination, return object with data and pagination
|
||||||
|
|||||||
@@ -130,6 +130,16 @@ export async function deleteProject(id: string): Promise<void> {
|
|||||||
return apiClient.delete<void>(`/api/projects/${id}`);
|
return apiClient.delete<void>(`/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<Project> {
|
||||||
|
const response = await apiClient.post<Record<string, unknown>>(`/api/projects/${id}/deactivate`, {});
|
||||||
|
return transformKeys<Project>(response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch unique area names from all projects
|
* Fetch unique area names from all projects
|
||||||
* @returns Promise resolving to an array of unique area names
|
* @returns Promise resolving to an array of unique area names
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createProject as apiCreateProject,
|
createProject as apiCreateProject,
|
||||||
updateProject as apiUpdateProject,
|
updateProject as apiUpdateProject,
|
||||||
deleteProject as apiDeleteProject,
|
deleteProject as apiDeleteProject,
|
||||||
|
deactivateProject as apiDeactivateProject,
|
||||||
} from "../../api/projects";
|
} from "../../api/projects";
|
||||||
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
|
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
|
||||||
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
|
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
|
||||||
@@ -27,6 +28,10 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
const [meterTypes, setMeterTypes] = useState<MeterType[]>([]);
|
const [meterTypes, setMeterTypes] = useState<MeterType[]>([]);
|
||||||
|
|
||||||
|
const [showDeactivateModal, setShowDeactivateModal] = useState(false);
|
||||||
|
const [projectToDeactivate, setProjectToDeactivate] = useState<Project | null>(null);
|
||||||
|
const [usersAssignedCount, setUsersAssignedCount] = useState(0);
|
||||||
|
|
||||||
const emptyForm: ProjectInput = {
|
const emptyForm: ProjectInput = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -119,12 +124,34 @@ export default function ProjectsPage() {
|
|||||||
setActiveProject(null);
|
setActiveProject(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting project:", error);
|
console.error("Error deleting project:", error);
|
||||||
|
|
||||||
|
// 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(
|
alert(
|
||||||
`Error deleting project: ${
|
`Error deleting project: ${errorMessage || "Please try again."}`
|
||||||
error instanceof Error ? error.message : "Please try again."
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
@@ -147,6 +174,31 @@ export default function ProjectsPage() {
|
|||||||
setShowModal(true);
|
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) =>
|
const filtered = visibleProjects.filter((p) =>
|
||||||
`${p.name} ${p.areaName} ${p.description ?? ""}`
|
`${p.name} ${p.areaName} ${p.description ?? ""}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -383,6 +435,60 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showDeactivateModal && projectToDeactivate && (
|
||||||
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-xl p-6 w-[500px] space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
|
Proyecto con Usuarios Asignados
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
El proyecto <span className="font-semibold">"{projectToDeactivate.name}"</span> tiene{" "}
|
||||||
|
<span className="font-semibold text-blue-600">{usersAssignedCount} usuario(s)</span> asignado(s).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
No se puede eliminar directamente. ¿Deseas continuar con las siguientes acciones?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="text-sm text-gray-700 space-y-2 bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-amber-600 font-bold">•</span>
|
||||||
|
<span>El proyecto será <span className="font-semibold">desactivado</span> (status = INACTIVE)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="text-amber-600 font-bold">•</span>
|
||||||
|
<span>Los <span className="font-semibold">{usersAssignedCount} usuario(s)</span> serán desasignados (project_id = null)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Nota: El proyecto no será eliminado de la base de datos, solo desactivado.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-3 border-t">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeactivateModal(false);
|
||||||
|
setProjectToDeactivate(null);
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmDeactivate}
|
||||||
|
className="bg-amber-600 text-white px-4 py-2 rounded hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
Sí, Desactivar Proyecto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,3 +225,37 @@ export async function getStats(req: Request, res: Response): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /projects/:id/deactivate
|
||||||
|
* Deactivate a project and unassign users
|
||||||
|
* Requires authentication
|
||||||
|
*/
|
||||||
|
export async function deactivateProject(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ router.put('/:id', authenticateToken, validateUpdateProject, projectController.u
|
|||||||
*/
|
*/
|
||||||
router.patch('/:id', authenticateToken, validateUpdateProject, projectController.update);
|
router.patch('/:id', authenticateToken, validateUpdateProject, projectController.update);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /projects/:id/deactivate
|
||||||
|
* Protected endpoint - deactivate a project and unassign users
|
||||||
|
* Headers: Authorization: Bearer <accessToken>
|
||||||
|
* Response: { success: true, data: Project }
|
||||||
|
*/
|
||||||
|
router.post('/:id/deactivate', authenticateToken, projectController.deactivateProject);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /projects/:id
|
* DELETE /projects/:id
|
||||||
* Protected endpoint - delete a project (requires admin role)
|
* Protected endpoint - delete a project (requires admin role)
|
||||||
|
|||||||
@@ -323,3 +323,25 @@ export async function getStats(id: string): Promise<ProjectStats | null> {
|
|||||||
concentrator_count: parseInt(concentratorStats.rows[0]?.count || '0', 10),
|
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<Project> {
|
||||||
|
await query(
|
||||||
|
'UPDATE users SET project_id = NULL WHERE project_id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await query<Project>(
|
||||||
|
`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];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user