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 { 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> { 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(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 { const result = await query( `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 { const result = await query( `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 { // 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( `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 { // 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 { // 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), }; }