diff --git a/water-api/sql/create_meter_types.sql b/water-api/sql/create_meter_types.sql new file mode 100644 index 0000000..44cdc69 --- /dev/null +++ b/water-api/sql/create_meter_types.sql @@ -0,0 +1,81 @@ +-- ============================================================================ +-- Create meter_types table and add relationship to projects +-- Meter types: LoRa, LoRaWAN, Grandes Consumidores +-- ============================================================================ + +-- Create meter_types table +CREATE TABLE IF NOT EXISTS meter_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(50) NOT NULL UNIQUE, + code VARCHAR(20) NOT NULL UNIQUE, + description TEXT, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Insert default meter types +INSERT INTO meter_types (name, code, description) VALUES + ('LoRa', 'LORA', 'Medidores con tecnologĂ­a LoRa'), + ('LoRaWAN', 'LORAWAN', 'Medidores con tecnologĂ­a LoRaWAN'), + ('Grandes Consumidores', 'GRANDES', 'Medidores para grandes consumidores') +ON CONFLICT (code) DO NOTHING; + +-- Add meter_type_id column to projects table +ALTER TABLE projects +ADD COLUMN IF NOT EXISTS meter_type_id UUID REFERENCES meter_types(id) ON DELETE SET NULL; + +-- Add index for better query performance +CREATE INDEX IF NOT EXISTS idx_projects_meter_type_id ON projects(meter_type_id); + +-- Add comment +COMMENT ON TABLE meter_types IS 'Catalog of meter types (LoRa, LoRaWAN, Grandes Consumidores)'; +COMMENT ON COLUMN projects.meter_type_id IS 'Default meter type for this project'; + +-- ============================================================================ +-- Helper function to get meter type by code +-- ============================================================================ + +CREATE OR REPLACE FUNCTION get_meter_type_id(type_code VARCHAR) +RETURNS UUID AS $$ +DECLARE + type_id UUID; +BEGIN + SELECT id INTO type_id FROM meter_types WHERE code = type_code AND is_active = true; + RETURN type_id; +END; +$$ LANGUAGE plpgsql; + +-- ============================================================================ +-- Update trigger for updated_at +-- ============================================================================ + +CREATE OR REPLACE FUNCTION update_meter_types_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_meter_types_updated_at + BEFORE UPDATE ON meter_types + FOR EACH ROW + EXECUTE FUNCTION update_meter_types_updated_at(); + +-- ============================================================================ +-- Verify the changes +-- ============================================================================ + +-- Show meter types +SELECT id, name, code, description, is_active FROM meter_types ORDER BY code; + +-- Show projects table structure +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_name = 'projects' + AND column_name = 'meter_type_id'; diff --git a/water-api/src/controllers/meterType.controller.ts b/water-api/src/controllers/meterType.controller.ts new file mode 100644 index 0000000..62bd218 --- /dev/null +++ b/water-api/src/controllers/meterType.controller.ts @@ -0,0 +1,185 @@ +import { Request, Response } from 'express'; +import * as meterTypeService from '../services/meterType.service'; + +/** + * GET /meter-types + * Get all active meter types + */ +export async function getAll(req: Request, res: Response): Promise { + try { + const meterTypes = await meterTypeService.getAll(); + + res.status(200).json({ + success: true, + data: meterTypes, + }); + } catch (error) { + console.error('Error fetching meter types:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch meter types', + }); + } +} + +/** + * GET /meter-types/:id + * Get a single meter type by ID + */ +export async function getById(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const meterType = await meterTypeService.getById(id); + + if (!meterType) { + res.status(404).json({ + success: false, + error: 'Meter type not found', + }); + return; + } + + res.status(200).json({ + success: true, + data: meterType, + }); + } catch (error) { + console.error('Error fetching meter type:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch meter type', + }); + } +} + +/** + * GET /meter-types/code/:code + * Get a meter type by code + */ +export async function getByCode(req: Request, res: Response): Promise { + try { + const { code } = req.params; + const meterType = await meterTypeService.getByCode(code); + + if (!meterType) { + res.status(404).json({ + success: false, + error: 'Meter type not found', + }); + return; + } + + res.status(200).json({ + success: true, + data: meterType, + }); + } catch (error) { + console.error('Error fetching meter type:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch meter type', + }); + } +} + +/** + * POST /meter-types + * Create a new meter type (ADMIN only) + */ +export async function create(req: Request, res: Response): Promise { + try { + const { name, code, description } = req.body; + + if (!name || !code) { + res.status(400).json({ + success: false, + error: 'Name and code are required', + }); + return; + } + + const meterType = await meterTypeService.create({ + name, + code, + description, + }); + + res.status(201).json({ + success: true, + data: meterType, + }); + } catch (error) { + console.error('Error creating meter type:', error); + res.status(500).json({ + success: false, + error: 'Failed to create meter type', + }); + } +} + +/** + * PUT /meter-types/:id + * Update a meter type (ADMIN only) + */ +export async function update(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const { name, code, description, is_active } = req.body; + + const meterType = await meterTypeService.update(id, { + name, + code, + description, + is_active, + }); + + if (!meterType) { + res.status(404).json({ + success: false, + error: 'Meter type not found', + }); + return; + } + + res.status(200).json({ + success: true, + data: meterType, + }); + } catch (error) { + console.error('Error updating meter type:', error); + res.status(500).json({ + success: false, + error: 'Failed to update meter type', + }); + } +} + +/** + * DELETE /meter-types/:id + * Soft delete a meter type (ADMIN only) + */ +export async function remove(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const success = await meterTypeService.remove(id); + + if (!success) { + res.status(404).json({ + success: false, + error: 'Meter type not found', + }); + return; + } + + res.status(200).json({ + success: true, + message: 'Meter type deleted successfully', + }); + } catch (error) { + console.error('Error deleting meter type:', error); + res.status(500).json({ + success: false, + error: 'Failed to delete meter type', + }); + } +} diff --git a/water-api/src/routes/index.ts b/water-api/src/routes/index.ts index 38f5a67..3f29cab 100644 --- a/water-api/src/routes/index.ts +++ b/water-api/src/routes/index.ts @@ -4,6 +4,7 @@ import { Router } from 'express'; import authRoutes from './auth.routes'; import projectRoutes from './project.routes'; import meterRoutes from './meter.routes'; +import meterTypeRoutes from './meterType.routes'; import concentratorRoutes from './concentrator.routes'; import gatewayRoutes from './gateway.routes'; import deviceRoutes from './device.routes'; @@ -52,6 +53,17 @@ router.use('/projects', projectRoutes); */ router.use('/meters', meterRoutes); +/** + * Meter Type routes: + * - GET /meter-types - List all meter types + * - GET /meter-types/:id - Get meter type by ID + * - GET /meter-types/code/:code - Get meter type by code + * - POST /meter-types - Create meter type (admin only) + * - PUT /meter-types/:id - Update meter type (admin only) + * - DELETE /meter-types/:id - Delete meter type (admin only) + */ +router.use('/meter-types', meterTypeRoutes); + /** * Concentrator routes: * - GET /concentrators - List all concentrators diff --git a/water-api/src/routes/meterType.routes.ts b/water-api/src/routes/meterType.routes.ts new file mode 100644 index 0000000..8b28f59 --- /dev/null +++ b/water-api/src/routes/meterType.routes.ts @@ -0,0 +1,64 @@ +import { Router } from 'express'; +import * as meterTypeController from '../controllers/meterType.controller'; +import { authenticateToken, requireRole } from '../middleware/auth.middleware'; + +const router = Router(); + +/** + * @route GET /api/meter-types + * @desc Get all active meter types + * @access Private (any authenticated user) + */ +router.get('/', authenticateToken, meterTypeController.getAll); + +/** + * @route GET /api/meter-types/code/:code + * @desc Get a meter type by code + * @access Private (any authenticated user) + */ +router.get('/code/:code', authenticateToken, meterTypeController.getByCode); + +/** + * @route GET /api/meter-types/:id + * @desc Get a single meter type by ID + * @access Private (any authenticated user) + */ +router.get('/:id', authenticateToken, meterTypeController.getById); + +/** + * @route POST /api/meter-types + * @desc Create a new meter type + * @access Private (ADMIN only) + */ +router.post( + '/', + authenticateToken, + requireRole('ADMIN'), + meterTypeController.create +); + +/** + * @route PUT /api/meter-types/:id + * @desc Update a meter type + * @access Private (ADMIN only) + */ +router.put( + '/:id', + authenticateToken, + requireRole('ADMIN'), + meterTypeController.update +); + +/** + * @route DELETE /api/meter-types/:id + * @desc Soft delete a meter type + * @access Private (ADMIN only) + */ +router.delete( + '/:id', + authenticateToken, + requireRole('ADMIN'), + meterTypeController.remove +); + +export default router; diff --git a/water-api/src/services/meterType.service.ts b/water-api/src/services/meterType.service.ts new file mode 100644 index 0000000..84d1a32 --- /dev/null +++ b/water-api/src/services/meterType.service.ts @@ -0,0 +1,142 @@ +import { query } from '../config/database'; +import { MeterType } from '../types'; + +/** + * Get all meter types + * @returns Promise resolving to array of meter types + */ +export async function getAll(): Promise { + const result = await query( + `SELECT id, name, code, description, is_active, created_at, updated_at + FROM meter_types + WHERE is_active = true + ORDER BY code ASC` + ); + + return result.rows; +} + +/** + * Get a single meter type by ID + * @param id - Meter type ID + * @returns Promise resolving to meter type or null if not found + */ +export async function getById(id: string): Promise { + const result = await query( + `SELECT id, name, code, description, is_active, created_at, updated_at + FROM meter_types + WHERE id = $1`, + [id] + ); + + return result.rows[0] || null; +} + +/** + * Get a meter type by code + * @param code - Meter type code (LORA, LORAWAN, GRANDES) + * @returns Promise resolving to meter type or null if not found + */ +export async function getByCode(code: string): Promise { + const result = await query( + `SELECT id, name, code, description, is_active, created_at, updated_at + FROM meter_types + WHERE code = $1 AND is_active = true`, + [code] + ); + + return result.rows[0] || null; +} + +/** + * Create a new meter type + * @param data - Meter type data + * @returns Promise resolving to created meter type + */ +export async function create(data: { + name: string; + code: string; + description?: string | null; +}): Promise { + const result = await query( + `INSERT INTO meter_types (name, code, description) + VALUES ($1, $2, $3) + RETURNING id, name, code, description, is_active, created_at, updated_at`, + [data.name, data.code, data.description || null] + ); + + return result.rows[0]; +} + +/** + * Update a meter type + * @param id - Meter type ID + * @param data - Fields to update + * @returns Promise resolving to updated meter type or null if not found + */ +export async function update( + id: string, + data: { + name?: string; + code?: string; + description?: string | null; + is_active?: boolean; + } +): Promise { + const updates: string[] = []; + const params: unknown[] = []; + let paramIndex = 1; + + if (data.name !== undefined) { + updates.push(`name = $${paramIndex}`); + params.push(data.name); + paramIndex++; + } + + if (data.code !== undefined) { + updates.push(`code = $${paramIndex}`); + params.push(data.code); + paramIndex++; + } + + if (data.description !== undefined) { + updates.push(`description = $${paramIndex}`); + params.push(data.description); + paramIndex++; + } + + if (data.is_active !== undefined) { + updates.push(`is_active = $${paramIndex}`); + params.push(data.is_active); + paramIndex++; + } + + if (updates.length === 0) { + return getById(id); + } + + params.push(id); + const result = await query( + `UPDATE meter_types + SET ${updates.join(', ')} + WHERE id = $${paramIndex} + RETURNING id, name, code, description, is_active, created_at, updated_at`, + params + ); + + return result.rows[0] || null; +} + +/** + * Delete a meter type (soft delete by setting is_active to false) + * @param id - Meter type ID + * @returns Promise resolving to boolean indicating success + */ +export async function remove(id: string): Promise { + const result = await query( + `UPDATE meter_types SET is_active = false WHERE id = $1`, + [id] + ); + + return result.rowCount ? result.rowCount > 0 : false; +} diff --git a/water-api/src/services/project.service.ts b/water-api/src/services/project.service.ts index 4226247..11916c1 100644 --- a/water-api/src/services/project.service.ts +++ b/water-api/src/services/project.service.ts @@ -103,7 +103,7 @@ export async function getAll( // Get paginated data const dataQuery = ` - SELECT id, name, description, area_name, location, status, created_by, created_at, updated_at + SELECT id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at FROM projects ${whereClause} ORDER BY created_at DESC @@ -131,7 +131,7 @@ export async function getAll( */ 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 + `SELECT id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at FROM projects WHERE id = $1`, [id] @@ -148,15 +148,16 @@ export async function getById(id: string): Promise { */ 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`, + `INSERT INTO projects (name, description, area_name, location, status, meter_type_id, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at`, [ data.name, data.description || null, data.area_name || null, data.location || null, data.status || 'ACTIVE', + data.meter_type_id || null, userId, ] ); @@ -206,6 +207,12 @@ export async function update(id: string, data: UpdateProjectInput): Promise