Files
GRH/water-api/src/services/project.service.ts
Exteban08 c81a18987f Migrar backend a PostgreSQL + Node.js/Express con nuevas funcionalidades
Backend (water-api/):
- Crear API REST completa con Express + TypeScript
- Implementar autenticación JWT con refresh tokens
- CRUD completo para: projects, concentrators, meters, gateways, devices, users, roles
- Agregar validación con Zod para todas las entidades
- Implementar webhooks para The Things Stack (LoRaWAN)
- Agregar endpoint de lecturas con filtros y resumen de consumo
- Implementar carga masiva de medidores via Excel (.xlsx)

Frontend:
- Crear cliente HTTP con manejo automático de JWT y refresh
- Actualizar todas las APIs para usar nuevo backend
- Agregar sistema de autenticación real (login, logout, me)
- Agregar selector de tipo (LORA, LoRaWAN, Grandes) en concentradores y medidores
- Agregar campo Meter ID en medidores
- Crear modal de carga masiva para medidores
- Agregar página de consumo con gráficas y filtros
- Corregir carga de proyectos independiente de datos existentes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 10:13:26 +00:00

309 lines
8.2 KiB
TypeScript

import { query } from '../config/database';
import { CreateProjectInput, UpdateProjectInput, ProjectStatusType } from '../validators/project.validator';
/**
* Project interface matching database schema
*/
export interface Project {
id: string;
name: string;
description: string | null;
area_name: string | null;
location: string | null;
status: ProjectStatusType;
created_by: string | null;
created_at: Date;
updated_at: Date;
}
/**
* Project statistics interface
*/
export interface ProjectStats {
meter_count: number;
device_count: number;
concentrator_count: number;
active_meters: number;
inactive_meters: number;
}
/**
* Pagination parameters interface
*/
export interface PaginationParams {
page: number;
pageSize: number;
}
/**
* Filter parameters for projects
*/
export interface ProjectFilters {
status?: ProjectStatusType;
area_name?: string;
search?: string;
}
/**
* Paginated result interface
*/
export interface PaginatedResult<T> {
data: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}
/**
* Get all projects with optional filtering and pagination
* @param filters - Optional filters for status and area_name
* @param pagination - Optional pagination parameters
* @returns Paginated list of projects
*/
export async function getAll(
filters?: ProjectFilters,
pagination?: PaginationParams
): Promise<PaginatedResult<Project>> {
const page = pagination?.page || 1;
const pageSize = pagination?.pageSize || 10;
const offset = (page - 1) * pageSize;
// Build WHERE clause dynamically
const conditions: string[] = [];
const params: unknown[] = [];
let paramIndex = 1;
if (filters?.status) {
conditions.push(`status = $${paramIndex}`);
params.push(filters.status);
paramIndex++;
}
if (filters?.area_name) {
conditions.push(`area_name ILIKE $${paramIndex}`);
params.push(`%${filters.area_name}%`);
paramIndex++;
}
if (filters?.search) {
conditions.push(`(name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`);
params.push(`%${filters.search}%`);
paramIndex++;
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Get total count
const countQuery = `SELECT COUNT(*) as total FROM projects ${whereClause}`;
const countResult = await query<{ total: string }>(countQuery, params);
const total = parseInt(countResult.rows[0]?.total || '0', 10);
// Get paginated data
const dataQuery = `
SELECT id, name, description, area_name, location, status, created_by, created_at, updated_at
FROM projects
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
params.push(pageSize, offset);
const result = await query<Project>(dataQuery, params);
return {
data: result.rows,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
}
/**
* Get a single project by ID
* @param id - Project UUID
* @returns Project or null if not found
*/
export async function getById(id: string): Promise<Project | null> {
const result = await query<Project>(
`SELECT id, name, description, area_name, location, status, created_by, created_at, updated_at
FROM projects
WHERE id = $1`,
[id]
);
return result.rows[0] || null;
}
/**
* Create a new project
* @param data - Project data
* @param userId - ID of the user creating the project
* @returns Created project
*/
export async function create(data: CreateProjectInput, userId: string): Promise<Project> {
const result = await query<Project>(
`INSERT INTO projects (name, description, area_name, location, status, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, description, area_name, location, status, created_by, created_at, updated_at`,
[
data.name,
data.description || null,
data.area_name || null,
data.location || null,
data.status || 'ACTIVE',
userId,
]
);
return result.rows[0];
}
/**
* Update an existing project
* @param id - Project UUID
* @param data - Updated project data
* @returns Updated project or null if not found
*/
export async function update(id: string, data: UpdateProjectInput): Promise<Project | null> {
// Build SET clause dynamically based on provided fields
const updates: string[] = [];
const params: unknown[] = [];
let paramIndex = 1;
if (data.name !== undefined) {
updates.push(`name = $${paramIndex}`);
params.push(data.name);
paramIndex++;
}
if (data.description !== undefined) {
updates.push(`description = $${paramIndex}`);
params.push(data.description);
paramIndex++;
}
if (data.area_name !== undefined) {
updates.push(`area_name = $${paramIndex}`);
params.push(data.area_name);
paramIndex++;
}
if (data.location !== undefined) {
updates.push(`location = $${paramIndex}`);
params.push(data.location);
paramIndex++;
}
if (data.status !== undefined) {
updates.push(`status = $${paramIndex}`);
params.push(data.status);
paramIndex++;
}
// Always update the updated_at timestamp
updates.push(`updated_at = NOW()`);
if (updates.length === 1) {
// Only updated_at was added, no actual data to update
return getById(id);
}
params.push(id);
const result = await query<Project>(
`UPDATE projects
SET ${updates.join(', ')}
WHERE id = $${paramIndex}
RETURNING id, name, description, area_name, location, status, created_by, created_at, updated_at`,
params
);
return result.rows[0] || null;
}
/**
* Delete a project by ID
* Checks for dependent meters/concentrators before deletion
* @param id - Project UUID
* @returns True if deleted, throws error if has dependencies
*/
export async function deleteProject(id: string): Promise<boolean> {
// Check for dependent meters
const meterCheck = await query<{ count: string }>(
'SELECT COUNT(*) as count FROM meters WHERE project_id = $1',
[id]
);
const meterCount = parseInt(meterCheck.rows[0]?.count || '0', 10);
if (meterCount > 0) {
throw new Error(`Cannot delete project: ${meterCount} meter(s) are associated with this project`);
}
// Check for dependent concentrators
const concentratorCheck = await query<{ count: string }>(
'SELECT COUNT(*) as count FROM concentrators WHERE project_id = $1',
[id]
);
const concentratorCount = parseInt(concentratorCheck.rows[0]?.count || '0', 10);
if (concentratorCount > 0) {
throw new Error(`Cannot delete project: ${concentratorCount} concentrator(s) are associated with this project`);
}
const result = await query('DELETE FROM projects WHERE id = $1', [id]);
return (result.rowCount || 0) > 0;
}
/**
* Get project statistics
* @param id - Project UUID
* @returns Project statistics including meter count, device count, etc.
*/
export async function getStats(id: string): Promise<ProjectStats | null> {
// Verify project exists
const project = await getById(id);
if (!project) {
return null;
}
// Get meter counts
const meterStats = await query<{ total: string; active: string; inactive: string }>(
`SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE status = 'ACTIVE' OR status = 'active') as active,
COUNT(*) FILTER (WHERE status = 'INACTIVE' OR status = 'inactive') as inactive
FROM meters
WHERE project_id = $1`,
[id]
);
// Get device count (devices linked to meters in this project)
const deviceStats = await query<{ count: string }>(
`SELECT COUNT(DISTINCT device_id) as count
FROM meters
WHERE project_id = $1 AND device_id IS NOT NULL`,
[id]
);
// Get concentrator count
const concentratorStats = await query<{ count: string }>(
'SELECT COUNT(*) as count FROM concentrators WHERE project_id = $1',
[id]
);
return {
meter_count: parseInt(meterStats.rows[0]?.total || '0', 10),
active_meters: parseInt(meterStats.rows[0]?.active || '0', 10),
inactive_meters: parseInt(meterStats.rows[0]?.inactive || '0', 10),
device_count: parseInt(deviceStats.rows[0]?.count || '0', 10),
concentrator_count: parseInt(concentratorStats.rows[0]?.count || '0', 10),
};
}