Add 3-level role permissions, organismos operadores, and Histórico de Tomas page
Implements the full ADMIN → ORGANISMO_OPERADOR → OPERATOR permission hierarchy with scope-filtered data access across all backend services. Adds organismos operadores management (ADMIN only) and a new Histórico page for viewing per-meter reading history with chart, consumption stats, and CSV export. Key changes: - Backend: 3-level scope filtering on all services (meters, readings, projects, users) - Backend: Protect GET /meters routes with authenticateToken for role-based filtering - Backend: Pass requestingUser to reading service for scoped meter readings - Frontend: New HistoricoPage with meter selector, AreaChart, paginated table - Frontend: Consumption cards (Actual, Pasado, Diferencial) above date filters - Frontend: Meter search by name, serial, location, CESPT account, cadastral key - Frontend: OrganismosPage, updated Sidebar with 3-level visibility - SQL migrations for organismos_operadores table and FK columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
66
water-api/sql/add_organismos_operadores.sql
Normal file
66
water-api/sql/add_organismos_operadores.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- ============================================
|
||||
-- Migration: Add Organismos Operadores (3-level hierarchy)
|
||||
-- Admin → Organismo Operador → Operador
|
||||
-- ============================================
|
||||
|
||||
-- 1. Add ORGANISMO_OPERADOR to role_name ENUM
|
||||
-- NOTE: ALTER TYPE ADD VALUE cannot run inside a transaction block
|
||||
ALTER TYPE role_name ADD VALUE IF NOT EXISTS 'ORGANISMO_OPERADOR';
|
||||
|
||||
-- 2. Create organismos_operadores table
|
||||
CREATE TABLE IF NOT EXISTS organismos_operadores (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
region VARCHAR(255),
|
||||
contact_name VARCHAR(255),
|
||||
contact_email VARCHAR(255),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add updated_at trigger
|
||||
CREATE TRIGGER set_organismos_operadores_updated_at
|
||||
BEFORE UPDATE ON organismos_operadores
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Index for active organismos
|
||||
CREATE INDEX IF NOT EXISTS idx_organismos_operadores_active ON organismos_operadores (is_active);
|
||||
|
||||
-- 3. Add organismo_operador_id FK to projects table
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN IF NOT EXISTS organismo_operador_id UUID REFERENCES organismos_operadores(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_organismo_operador_id ON projects (organismo_operador_id);
|
||||
|
||||
-- 4. Add organismo_operador_id FK to users table
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS organismo_operador_id UUID REFERENCES organismos_operadores(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_organismo_operador_id ON users (organismo_operador_id);
|
||||
|
||||
-- 5. Insert ORGANISMO_OPERADOR role with permissions
|
||||
INSERT INTO roles (name, description, permissions)
|
||||
SELECT
|
||||
'ORGANISMO_OPERADOR',
|
||||
'Organismo operador que gestiona proyectos y operadores dentro de su jurisdicción',
|
||||
'["projects:read", "projects:list", "concentrators:read", "concentrators:list", "meters:read", "meters:write", "meters:list", "readings:read", "readings:list", "users:read", "users:write", "users:list"]'::jsonb
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM roles WHERE name = 'ORGANISMO_OPERADOR'
|
||||
);
|
||||
|
||||
-- 6. Migrate VIEWER users to OPERATOR role
|
||||
UPDATE users
|
||||
SET role_id = (SELECT id FROM roles WHERE name = 'OPERATOR' LIMIT 1)
|
||||
WHERE role_id = (SELECT id FROM roles WHERE name = 'VIEWER' LIMIT 1);
|
||||
|
||||
-- 7. Seed example organismos operadores
|
||||
INSERT INTO organismos_operadores (name, description, region, contact_name, contact_email)
|
||||
SELECT 'CESPT', 'Comisión Estatal de Servicios Públicos de Tijuana', 'Tijuana, BC', 'Admin CESPT', 'admin@cespt.gob.mx'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM organismos_operadores WHERE name = 'CESPT');
|
||||
|
||||
INSERT INTO organismos_operadores (name, description, region, contact_name, contact_email)
|
||||
SELECT 'XICALI', 'Organismo Operador de Mexicali', 'Mexicali, BC', 'Admin XICALI', 'admin@xicali.gob.mx'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM organismos_operadores WHERE name = 'XICALI');
|
||||
11
water-api/sql/add_user_meter_fields.sql
Normal file
11
water-api/sql/add_user_meter_fields.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Add new fields to users table
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS street VARCHAR(255);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS city VARCHAR(100);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(100);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS zip_code VARCHAR(10);
|
||||
|
||||
-- Add new fields to meters table
|
||||
ALTER TABLE meters ADD COLUMN IF NOT EXISTS address TEXT;
|
||||
ALTER TABLE meters ADD COLUMN IF NOT EXISTS cespt_account VARCHAR(50);
|
||||
ALTER TABLE meters ADD COLUMN IF NOT EXISTS cadastral_key VARCHAR(50);
|
||||
@@ -27,7 +27,8 @@ export async function getAll(req: AuthenticatedRequest, res: Response): Promise<
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await concentratorService.getAll(filters, pagination, requestingUser);
|
||||
|
||||
@@ -38,7 +38,8 @@ export async function getAll(req: AuthenticatedRequest, res: Response): Promise<
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await meterService.getAll(filters, { page, pageSize }, requestingUser);
|
||||
@@ -243,7 +244,7 @@ export async function deleteMeter(req: AuthenticatedRequest, res: Response): Pro
|
||||
* Get meter readings history with optional date range filter
|
||||
* Query params: start_date, end_date, page, pageSize
|
||||
*/
|
||||
export async function getReadings(req: Request, res: Response): Promise<void> {
|
||||
export async function getReadings(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
@@ -273,7 +274,14 @@ export async function getReadings(req: Request, res: Response): Promise<void> {
|
||||
filters.end_date = req.query.end_date as string;
|
||||
}
|
||||
|
||||
const result = await readingService.getAll(filters, { page, pageSize });
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await readingService.getAll(filters, { page, pageSize }, requestingUser);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
|
||||
186
water-api/src/controllers/organismo-operador.controller.ts
Normal file
186
water-api/src/controllers/organismo-operador.controller.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as organismoService from '../services/organismo-operador.service';
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores
|
||||
* List all organismos operadores
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string, 10) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 50, 100);
|
||||
|
||||
const result = await organismoService.getAll({ page, pageSize });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: result.pagination,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching organismos:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch organismos operadores',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores/:id
|
||||
* Get a single organismo by ID
|
||||
*/
|
||||
export async function getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const organismo = await organismoService.getById(id);
|
||||
|
||||
if (!organismo) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Organismo operador not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: organismo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching organismo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch organismo operador',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /organismos-operadores
|
||||
* Create a new organismo operador (ADMIN only)
|
||||
*/
|
||||
export async function create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = req.body as organismoService.CreateOrganismoInput;
|
||||
|
||||
const organismo = await organismoService.create(data);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: organismo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating organismo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create organismo operador',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /organismos-operadores/:id
|
||||
* Update an organismo operador (ADMIN only)
|
||||
*/
|
||||
export async function update(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = req.body as organismoService.UpdateOrganismoInput;
|
||||
|
||||
const organismo = await organismoService.update(id, data);
|
||||
|
||||
if (!organismo) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Organismo operador not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: organismo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating organismo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update organismo operador',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /organismos-operadores/:id
|
||||
* Delete an organismo operador (ADMIN only)
|
||||
*/
|
||||
export async function remove(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await organismoService.remove(id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Organismo operador not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { message: 'Organismo operador deleted successfully' },
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete organismo operador';
|
||||
|
||||
if (message.includes('Cannot delete')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Error deleting organismo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete organismo operador',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores/:id/projects
|
||||
* Get projects belonging to an organismo
|
||||
*/
|
||||
export async function getProjects(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const organismo = await organismoService.getById(id);
|
||||
if (!organismo) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Organismo operador not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const projects = await organismoService.getProjectsByOrganismo(id);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: projects,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching organismo projects:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch organismo projects',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { CreateProjectInput, UpdateProjectInput, ProjectStatusType } from '../va
|
||||
* List all projects with pagination and optional filtering
|
||||
* Query params: page, pageSize, status, area_name, search
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
export async function getAll(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string, 10) || 1;
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 10, 100);
|
||||
@@ -27,7 +27,14 @@ export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
filters.search = req.query.search as string;
|
||||
}
|
||||
|
||||
const result = await projectService.getAll(filters, { page, pageSize });
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await projectService.getAll(filters, { page, pageSize }, requestingUser);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
||||
import * as readingService from '../services/reading.service';
|
||||
|
||||
/**
|
||||
* GET /readings
|
||||
* List all readings with pagination and filtering
|
||||
*/
|
||||
export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
export async function getAll(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
page = '1',
|
||||
@@ -31,7 +32,14 @@ export async function getAll(req: Request, res: Response): Promise<void> {
|
||||
pageSize: Math.min(parseInt(pageSize as string, 10), 100), // Max 100 per page
|
||||
};
|
||||
|
||||
const result = await readingService.getAll(filters, pagination);
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await readingService.getAll(filters, pagination, requestingUser);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -136,12 +144,20 @@ export async function deleteReading(req: Request, res: Response): Promise<void>
|
||||
* GET /readings/summary
|
||||
* Get consumption summary statistics
|
||||
*/
|
||||
export async function getSummary(req: Request, res: Response): Promise<void> {
|
||||
export async function getSummary(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { project_id } = req.query;
|
||||
|
||||
// Pass user info for role-based filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
projectId: req.user.projectId,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const summary = await readingService.getConsumptionSummary(
|
||||
project_id as string | undefined
|
||||
project_id as string | undefined,
|
||||
requestingUser
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
@@ -41,7 +41,13 @@ export async function getAllUsers(
|
||||
sortOrder: (req.query.sortOrder as 'asc' | 'desc') || 'desc',
|
||||
};
|
||||
|
||||
const result = await userService.getAll(filters, pagination);
|
||||
// Pass requesting user for scope filtering
|
||||
const requestingUser = req.user ? {
|
||||
roleName: req.user.roleName,
|
||||
organismoOperadorId: req.user.organismoOperadorId,
|
||||
} : undefined;
|
||||
|
||||
const result = await userService.getAll(filters, pagination, requestingUser);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -125,12 +131,20 @@ export async function createUser(
|
||||
try {
|
||||
const data = req.body as CreateUserInput;
|
||||
|
||||
// If ORGANISMO_OPERADOR is creating a user, force their own organismo_operador_id
|
||||
let organismoOperadorId = data.organismo_operador_id;
|
||||
if (req.user?.roleName === 'ORGANISMO_OPERADOR' && req.user?.organismoOperadorId) {
|
||||
organismoOperadorId = req.user.organismoOperadorId;
|
||||
}
|
||||
|
||||
const user = await userService.create({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
name: data.name,
|
||||
avatar_url: data.avatar_url,
|
||||
role_id: data.role_id,
|
||||
project_id: data.project_id,
|
||||
organismo_operador_id: organismoOperadorId,
|
||||
is_active: data.is_active,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Response, NextFunction } from 'express';
|
||||
import { verifyAccessToken } from '../utils/jwt';
|
||||
import { AuthenticatedRequest } from '../types';
|
||||
|
||||
export { AuthenticatedRequest };
|
||||
|
||||
/**
|
||||
* Middleware to authenticate JWT access tokens
|
||||
* Extracts Bearer token from Authorization header, verifies it,
|
||||
@@ -42,6 +44,7 @@ export function authenticateToken(
|
||||
roleId: (decoded as any).roleId || (decoded as any).role,
|
||||
roleName: (decoded as any).roleName || (decoded as any).role,
|
||||
projectId: (decoded as any).projectId,
|
||||
organismoOperadorId: (decoded as any).organismoOperadorId,
|
||||
};
|
||||
|
||||
next();
|
||||
|
||||
@@ -16,6 +16,7 @@ import bulkUploadRoutes from './bulk-upload.routes';
|
||||
import csvUploadRoutes from './csv-upload.routes';
|
||||
import auditRoutes from './audit.routes';
|
||||
import notificationRoutes from './notification.routes';
|
||||
import organismoOperadorRoutes from './organismo-operador.routes';
|
||||
import testRoutes from './test.routes';
|
||||
import systemRoutes from './system.routes';
|
||||
|
||||
@@ -119,6 +120,17 @@ router.use('/users', userRoutes);
|
||||
*/
|
||||
router.use('/roles', roleRoutes);
|
||||
|
||||
/**
|
||||
* Organismos Operadores routes:
|
||||
* - GET /organismos-operadores - List all organismos
|
||||
* - GET /organismos-operadores/:id - Get organismo by ID
|
||||
* - GET /organismos-operadores/:id/projects - Get organismo's projects
|
||||
* - POST /organismos-operadores - Create organismo (admin only)
|
||||
* - PUT /organismos-operadores/:id - Update organismo (admin only)
|
||||
* - DELETE /organismos-operadores/:id - Delete organismo (admin only)
|
||||
*/
|
||||
router.use('/organismos-operadores', organismoOperadorRoutes);
|
||||
|
||||
/**
|
||||
* TTS (The Things Stack) webhook routes:
|
||||
* - GET /webhooks/tts/health - Health check
|
||||
|
||||
@@ -7,26 +7,26 @@ const router = Router();
|
||||
|
||||
/**
|
||||
* GET /meters
|
||||
* Public endpoint - list all meters with pagination and filtering
|
||||
* Protected endpoint - list meters filtered by user role/scope
|
||||
* Query params: page, pageSize, project_id, status, area_name, meter_type, search
|
||||
* Response: { success: true, data: Meter[], pagination: {...} }
|
||||
*/
|
||||
router.get('/', meterController.getAll);
|
||||
router.get('/', authenticateToken, meterController.getAll);
|
||||
|
||||
/**
|
||||
* GET /meters/:id
|
||||
* Public endpoint - get a single meter by ID with device info
|
||||
* Protected endpoint - get a single meter by ID with device info
|
||||
* Response: { success: true, data: MeterWithDevice }
|
||||
*/
|
||||
router.get('/:id', meterController.getById);
|
||||
router.get('/:id', authenticateToken, meterController.getById);
|
||||
|
||||
/**
|
||||
* GET /meters/:id/readings
|
||||
* Public endpoint - get meter readings history
|
||||
* Query params: start_date, end_date
|
||||
* Response: { success: true, data: MeterReading[] }
|
||||
* Protected endpoint - get meter readings history filtered by user role/scope
|
||||
* Query params: start_date, end_date, page, pageSize
|
||||
* Response: { success: true, data: MeterReading[], pagination: {...} }
|
||||
*/
|
||||
router.get('/:id/readings', meterController.getReadings);
|
||||
router.get('/:id/readings', authenticateToken, meterController.getReadings);
|
||||
|
||||
/**
|
||||
* POST /meters
|
||||
|
||||
48
water-api/src/routes/organismo-operador.routes.ts
Normal file
48
water-api/src/routes/organismo-operador.routes.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticateToken, requireRole } from '../middleware/auth.middleware';
|
||||
import * as organismoController from '../controllers/organismo-operador.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* All routes require authentication
|
||||
*/
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores
|
||||
* List all organismos operadores (ADMIN and ORGANISMO_OPERADOR)
|
||||
*/
|
||||
router.get('/', requireRole('ADMIN', 'ORGANISMO_OPERADOR'), organismoController.getAll);
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores/:id
|
||||
* Get a single organismo by ID
|
||||
*/
|
||||
router.get('/:id', requireRole('ADMIN', 'ORGANISMO_OPERADOR'), organismoController.getById);
|
||||
|
||||
/**
|
||||
* GET /organismos-operadores/:id/projects
|
||||
* Get projects belonging to an organismo
|
||||
*/
|
||||
router.get('/:id/projects', requireRole('ADMIN', 'ORGANISMO_OPERADOR'), organismoController.getProjects);
|
||||
|
||||
/**
|
||||
* POST /organismos-operadores
|
||||
* Create a new organismo operador (ADMIN only)
|
||||
*/
|
||||
router.post('/', requireRole('ADMIN'), organismoController.create);
|
||||
|
||||
/**
|
||||
* PUT /organismos-operadores/:id
|
||||
* Update an organismo operador (ADMIN only)
|
||||
*/
|
||||
router.put('/:id', requireRole('ADMIN'), organismoController.update);
|
||||
|
||||
/**
|
||||
* DELETE /organismos-operadores/:id
|
||||
* Delete an organismo operador (ADMIN only)
|
||||
*/
|
||||
router.delete('/:id', requireRole('ADMIN'), organismoController.remove);
|
||||
|
||||
export default router;
|
||||
@@ -11,7 +11,7 @@ const router = Router();
|
||||
* Query params: page, pageSize, status, area_name, search
|
||||
* Response: { success: true, data: Project[], pagination: {...} }
|
||||
*/
|
||||
router.get('/', projectController.getAll);
|
||||
router.get('/', authenticateToken, projectController.getAll);
|
||||
|
||||
/**
|
||||
* GET /projects/:id
|
||||
|
||||
@@ -10,15 +10,15 @@ const router = Router();
|
||||
* Query params: project_id
|
||||
* Response: { success: true, data: { totalReadings, totalMeters, avgReading, lastReadingDate } }
|
||||
*/
|
||||
router.get('/summary', readingController.getSummary);
|
||||
router.get('/summary', authenticateToken, readingController.getSummary);
|
||||
|
||||
/**
|
||||
* GET /readings
|
||||
* Public endpoint - list all readings with pagination and filtering
|
||||
* Protected endpoint - list all readings with pagination and filtering
|
||||
* Query params: page, pageSize, meter_id, project_id, area_name, start_date, end_date, reading_type
|
||||
* Response: { success: true, data: Reading[], pagination: {...} }
|
||||
*/
|
||||
router.get('/', readingController.getAll);
|
||||
router.get('/', authenticateToken, readingController.getAll);
|
||||
|
||||
/**
|
||||
* GET /readings/:id
|
||||
|
||||
@@ -20,7 +20,7 @@ router.use(authenticateToken);
|
||||
* Query params: role_id, is_active, search, page, limit, sortBy, sortOrder
|
||||
* Response: { success, message, data: User[], pagination }
|
||||
*/
|
||||
router.get('/', requireRole('ADMIN'), userController.getAllUsers);
|
||||
router.get('/', requireRole('ADMIN', 'ORGANISMO_OPERADOR'), userController.getAllUsers);
|
||||
|
||||
/**
|
||||
* GET /users/:id
|
||||
@@ -35,7 +35,7 @@ router.get('/:id', userController.getUserById);
|
||||
* Body: { email, password, name, avatar_url?, role_id, is_active? }
|
||||
* Response: { success, message, data: User }
|
||||
*/
|
||||
router.post('/', requireRole('ADMIN'), validateCreateUser, userController.createUser);
|
||||
router.post('/', requireRole('ADMIN', 'ORGANISMO_OPERADOR'), validateCreateUser, userController.createUser);
|
||||
|
||||
/**
|
||||
* PUT /users/:id
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface UserProfile {
|
||||
role: string;
|
||||
avatarUrl?: string | null;
|
||||
projectId?: string | null;
|
||||
organismoOperadorId?: string | null;
|
||||
organismoName?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ export async function login(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<LoginResult> {
|
||||
// Find user by email with role name
|
||||
// Find user by email with role name and organismo
|
||||
const userResult = await query<{
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -57,9 +59,10 @@ export async function login(
|
||||
avatar_url: string | null;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
created_at: Date;
|
||||
}>(
|
||||
`SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_id, u.created_at
|
||||
`SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_id, u.organismo_operador_id, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE LOWER(u.email) = LOWER($1) AND u.is_active = true
|
||||
@@ -86,6 +89,7 @@ export async function login(
|
||||
roleId: user.id,
|
||||
roleName: user.role_name,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
});
|
||||
|
||||
const refreshToken = generateRefreshToken({
|
||||
@@ -174,8 +178,9 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
||||
email: string;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
}>(
|
||||
`SELECT u.id, u.email, r.name as role_name, u.project_id
|
||||
`SELECT u.id, u.email, r.name as role_name, u.project_id, u.organismo_operador_id
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = $1 AND u.is_active = true
|
||||
@@ -195,6 +200,7 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
||||
roleId: user.id,
|
||||
roleName: user.role_name,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
});
|
||||
|
||||
return { accessToken };
|
||||
@@ -232,11 +238,15 @@ export async function getMe(userId: string): Promise<UserProfile> {
|
||||
avatar_url: string | null;
|
||||
role_name: string;
|
||||
project_id: string | null;
|
||||
organismo_operador_id: string | null;
|
||||
organismo_name: string | null;
|
||||
created_at: Date;
|
||||
}>(
|
||||
`SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id, u.created_at
|
||||
`SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id,
|
||||
u.organismo_operador_id, oo.name as organismo_name, u.created_at
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN organismos_operadores oo ON u.organismo_operador_id = oo.id
|
||||
WHERE u.id = $1 AND u.is_active = true
|
||||
LIMIT 1`,
|
||||
[userId]
|
||||
@@ -255,6 +265,8 @@ export async function getMe(userId: string): Promise<UserProfile> {
|
||||
role: user.role_name,
|
||||
avatarUrl: user.avatar_url,
|
||||
projectId: user.project_id,
|
||||
organismoOperadorId: user.organismo_operador_id,
|
||||
organismoName: user.organismo_name,
|
||||
createdAt: user.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export interface PaginatedResult<T> {
|
||||
export async function getAll(
|
||||
filters?: ConcentratorFilters,
|
||||
pagination?: PaginationOptions,
|
||||
requestingUser?: { roleName: string; projectId?: string | null }
|
||||
requestingUser?: { roleName: string; projectId?: string | null; organismoOperadorId?: string | null }
|
||||
): Promise<PaginatedResult<Concentrator>> {
|
||||
const page = pagination?.page || 1;
|
||||
const limit = pagination?.limit || 10;
|
||||
@@ -89,15 +89,19 @@ export async function getAll(
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Role-based filtering: OPERATOR users can only see their assigned project
|
||||
if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
// Role-based filtering: 3-level hierarchy
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`project_id IN (SELECT id FROM projects WHERE organismo_operador_id = $${paramIndex})`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
} else if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
conditions.push(`project_id = $${paramIndex}`);
|
||||
params.push(requestingUser.projectId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Additional filter by project_id (only applies if user is ADMIN or no user context)
|
||||
if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN')) {
|
||||
// Additional filter by project_id (applies if user is ADMIN, ORGANISMO_OPERADOR, or no user context)
|
||||
if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN' || requestingUser.roleName === 'ORGANISMO_OPERADOR')) {
|
||||
conditions.push(`project_id = $${paramIndex}`);
|
||||
params.push(filters.project_id);
|
||||
paramIndex++;
|
||||
|
||||
@@ -73,6 +73,11 @@ export interface Meter {
|
||||
|
||||
// Additional Data
|
||||
data?: Record<string, any> | null;
|
||||
|
||||
// Address & Account Fields
|
||||
address?: string | null;
|
||||
cespt_account?: string | null;
|
||||
cadastral_key?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +170,9 @@ export interface CreateMeterInput {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
data?: Record<string, any>;
|
||||
address?: string;
|
||||
cespt_account?: string;
|
||||
cadastral_key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,6 +224,9 @@ export interface UpdateMeterInput {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
data?: Record<string, any>;
|
||||
address?: string;
|
||||
cespt_account?: string;
|
||||
cadastral_key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +238,7 @@ export interface UpdateMeterInput {
|
||||
export async function getAll(
|
||||
filters?: MeterFilters,
|
||||
pagination?: PaginationParams,
|
||||
requestingUser?: { roleName: string; projectId?: string | null }
|
||||
requestingUser?: { roleName: string; projectId?: string | null; organismoOperadorId?: string | null }
|
||||
): Promise<PaginatedResult<MeterWithDetails>> {
|
||||
const page = pagination?.page || 1;
|
||||
const pageSize = pagination?.pageSize || 50;
|
||||
@@ -237,8 +248,12 @@ export async function getAll(
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Role-based filtering: OPERATOR users can only see meters from their assigned project
|
||||
if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
// Role-based filtering: 3-level hierarchy
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`c.project_id IN (SELECT id FROM projects WHERE organismo_operador_id = $${paramIndex})`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
} else if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
conditions.push(`c.project_id = $${paramIndex}`);
|
||||
params.push(requestingUser.projectId);
|
||||
paramIndex++;
|
||||
@@ -250,8 +265,8 @@ export async function getAll(
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Additional filter by project_id (only applies if user is ADMIN or no user context)
|
||||
if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN')) {
|
||||
// Additional filter by project_id (applies if user is ADMIN, ORGANISMO_OPERADOR, or no user context)
|
||||
if (filters?.project_id && (!requestingUser || requestingUser.roleName === 'ADMIN' || requestingUser.roleName === 'ORGANISMO_OPERADOR')) {
|
||||
conditions.push(`c.project_id = $${paramIndex}`);
|
||||
params.push(filters.project_id);
|
||||
paramIndex++;
|
||||
@@ -296,7 +311,8 @@ export async function getAll(
|
||||
c.name as concentrator_name, c.serial_number as concentrator_serial,
|
||||
c.project_id, p.name as project_name,
|
||||
m.protocol, m.voltage, m.signal, m.leakage_status, m.burst_status,
|
||||
m.current_flow, m.total_flow_reverse, m.manufacturer, m.latitude, m.longitude
|
||||
m.current_flow, m.total_flow_reverse, m.manufacturer, m.latitude, m.longitude,
|
||||
m.address, m.cespt_account, m.cadastral_key
|
||||
FROM meters m
|
||||
JOIN concentrators c ON m.concentrator_id = c.id
|
||||
JOIN projects p ON c.project_id = p.id
|
||||
@@ -329,7 +345,8 @@ export async function getById(id: string): Promise<MeterWithDetails | null> {
|
||||
m.status, m.last_reading_value, m.last_reading_at, m.installation_date,
|
||||
m.created_at, m.updated_at,
|
||||
c.name as concentrator_name, c.serial_number as concentrator_serial,
|
||||
c.project_id, p.name as project_name
|
||||
c.project_id, p.name as project_name,
|
||||
m.address, m.cespt_account, m.cadastral_key
|
||||
FROM meters m
|
||||
JOIN concentrators c ON m.concentrator_id = c.id
|
||||
JOIN projects p ON c.project_id = p.id
|
||||
@@ -360,8 +377,8 @@ export async function create(data: CreateMeterInput): Promise<Meter> {
|
||||
}
|
||||
|
||||
const result = await query<Meter>(
|
||||
`INSERT INTO meters (serial_number, meter_id, name, project_id, concentrator_id, location, type, status, installation_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`INSERT INTO meters (serial_number, meter_id, name, project_id, concentrator_id, location, type, status, installation_date, address, cespt_account, cadastral_key)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.serial_number,
|
||||
@@ -373,6 +390,9 @@ export async function create(data: CreateMeterInput): Promise<Meter> {
|
||||
data.type || 'LORA',
|
||||
data.status || 'ACTIVE',
|
||||
data.installation_date || null,
|
||||
data.address || null,
|
||||
data.cespt_account || null,
|
||||
data.cadastral_key || null,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -454,6 +474,24 @@ export async function update(id: string, data: UpdateMeterInput): Promise<Meter
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.address !== undefined) {
|
||||
updates.push(`address = $${paramIndex}`);
|
||||
params.push(data.address);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.cespt_account !== undefined) {
|
||||
updates.push(`cespt_account = $${paramIndex}`);
|
||||
params.push(data.cespt_account);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.cadastral_key !== undefined) {
|
||||
updates.push(`cadastral_key = $${paramIndex}`);
|
||||
params.push(data.cadastral_key);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`);
|
||||
|
||||
if (updates.length === 1) {
|
||||
|
||||
@@ -100,7 +100,7 @@ export async function getAllForUser(
|
||||
ORDER BY is_read ASC, ${safeSortBy} ${safeSortOrder}
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`;
|
||||
const dataResult = await query(dataQuery, [...params, limit, offset]);
|
||||
const dataResult = await query<Notification>(dataQuery, [...params, limit, offset]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
@@ -144,7 +144,7 @@ export async function getById(id: string, userId: string): Promise<Notification
|
||||
FROM notifications
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`;
|
||||
const result = await query(sql, [id, userId]);
|
||||
const result = await query<Notification>(sql, [id, userId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export async function create(input: CreateNotificationInput): Promise<Notificati
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await query(sql, [
|
||||
const result = await query<Notification>(sql, [
|
||||
input.user_id,
|
||||
input.meter_id || null,
|
||||
input.notification_type,
|
||||
@@ -176,7 +176,7 @@ export async function create(input: CreateNotificationInput): Promise<Notificati
|
||||
input.meter_serial_number || null,
|
||||
input.flow_value || null,
|
||||
]);
|
||||
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function markAsRead(id: string, userId: string): Promise<Notificati
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await query(sql, [id, userId]);
|
||||
const result = await query<Notification>(sql, [id, userId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ export async function getMetersWithNegativeFlow(): Promise<Array<{
|
||||
WHERE m.last_reading_value < 0
|
||||
AND m.status = 'ACTIVE'
|
||||
`;
|
||||
const result = await query(sql);
|
||||
const result = await query<{ id: string; serial_number: string; name: string; last_reading_value: number; concentrator_id: string; project_id: string }>(sql);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
|
||||
224
water-api/src/services/organismo-operador.service.ts
Normal file
224
water-api/src/services/organismo-operador.service.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { query } from '../config/database';
|
||||
|
||||
export interface OrganismoOperador {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
region: string | null;
|
||||
contact_name: string | null;
|
||||
contact_email: string | null;
|
||||
is_active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface OrganismoOperadorWithStats extends OrganismoOperador {
|
||||
project_count: number;
|
||||
user_count: number;
|
||||
}
|
||||
|
||||
export interface CreateOrganismoInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
region?: string;
|
||||
contact_name?: string;
|
||||
contact_email?: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateOrganismoInput {
|
||||
name?: string;
|
||||
description?: string;
|
||||
region?: string;
|
||||
contact_name?: string;
|
||||
contact_email?: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all organismos operadores with pagination
|
||||
*/
|
||||
export async function getAll(
|
||||
pagination?: PaginationParams
|
||||
): Promise<PaginatedResult<OrganismoOperadorWithStats>> {
|
||||
const page = pagination?.page || 1;
|
||||
const pageSize = pagination?.pageSize || 50;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const countResult = await query<{ total: string }>(
|
||||
'SELECT COUNT(*) as total FROM organismos_operadores'
|
||||
);
|
||||
const total = parseInt(countResult.rows[0]?.total || '0', 10);
|
||||
|
||||
const result = await query<OrganismoOperadorWithStats>(
|
||||
`SELECT oo.*,
|
||||
COALESCE((SELECT COUNT(*) FROM projects p WHERE p.organismo_operador_id = oo.id), 0)::int as project_count,
|
||||
COALESCE((SELECT COUNT(*) FROM users u WHERE u.organismo_operador_id = oo.id), 0)::int as user_count
|
||||
FROM organismos_operadores oo
|
||||
ORDER BY oo.created_at DESC
|
||||
LIMIT $1 OFFSET $2`,
|
||||
[pageSize, offset]
|
||||
);
|
||||
|
||||
return {
|
||||
data: result.rows,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single organismo by ID with stats
|
||||
*/
|
||||
export async function getById(id: string): Promise<OrganismoOperadorWithStats | null> {
|
||||
const result = await query<OrganismoOperadorWithStats>(
|
||||
`SELECT oo.*,
|
||||
COALESCE((SELECT COUNT(*) FROM projects p WHERE p.organismo_operador_id = oo.id), 0)::int as project_count,
|
||||
COALESCE((SELECT COUNT(*) FROM users u WHERE u.organismo_operador_id = oo.id), 0)::int as user_count
|
||||
FROM organismos_operadores oo
|
||||
WHERE oo.id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new organismo operador
|
||||
*/
|
||||
export async function create(data: CreateOrganismoInput): Promise<OrganismoOperador> {
|
||||
const result = await query<OrganismoOperador>(
|
||||
`INSERT INTO organismos_operadores (name, description, region, contact_name, contact_email, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.name,
|
||||
data.description || null,
|
||||
data.region || null,
|
||||
data.contact_name || null,
|
||||
data.contact_email || null,
|
||||
data.is_active ?? true,
|
||||
]
|
||||
);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing organismo operador
|
||||
*/
|
||||
export async function update(id: string, data: UpdateOrganismoInput): Promise<OrganismoOperador | null> {
|
||||
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.region !== undefined) {
|
||||
updates.push(`region = $${paramIndex}`);
|
||||
params.push(data.region);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.contact_name !== undefined) {
|
||||
updates.push(`contact_name = $${paramIndex}`);
|
||||
params.push(data.contact_name);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.contact_email !== undefined) {
|
||||
updates.push(`contact_email = $${paramIndex}`);
|
||||
params.push(data.contact_email);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.is_active !== undefined) {
|
||||
updates.push(`is_active = $${paramIndex}`);
|
||||
params.push(data.is_active);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return getById(id) as Promise<OrganismoOperador | null>;
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`);
|
||||
params.push(id);
|
||||
|
||||
const result = await query<OrganismoOperador>(
|
||||
`UPDATE organismos_operadores SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an organismo operador
|
||||
*/
|
||||
export async function remove(id: string): Promise<boolean> {
|
||||
// Check for dependent projects
|
||||
const projectCheck = await query<{ count: string }>(
|
||||
'SELECT COUNT(*) as count FROM projects WHERE organismo_operador_id = $1',
|
||||
[id]
|
||||
);
|
||||
const projectCount = parseInt(projectCheck.rows[0]?.count || '0', 10);
|
||||
|
||||
if (projectCount > 0) {
|
||||
throw new Error(`Cannot delete organismo: ${projectCount} project(s) are associated with it`);
|
||||
}
|
||||
|
||||
// Check for dependent users
|
||||
const userCheck = await query<{ count: string }>(
|
||||
'SELECT COUNT(*) as count FROM users WHERE organismo_operador_id = $1',
|
||||
[id]
|
||||
);
|
||||
const userCount = parseInt(userCheck.rows[0]?.count || '0', 10);
|
||||
|
||||
if (userCount > 0) {
|
||||
throw new Error(`Cannot delete organismo: ${userCount} user(s) are associated with it`);
|
||||
}
|
||||
|
||||
const result = await query('DELETE FROM organismos_operadores WHERE id = $1', [id]);
|
||||
return (result.rowCount || 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projects belonging to an organismo
|
||||
*/
|
||||
export async function getProjectsByOrganismo(organismoId: string): Promise<{ id: string; name: string; status: string }[]> {
|
||||
const result = await query<{ id: string; name: string; status: string }>(
|
||||
'SELECT id, name, status FROM projects WHERE organismo_operador_id = $1 ORDER BY name',
|
||||
[organismoId]
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
@@ -65,7 +65,8 @@ export interface PaginatedResult<T> {
|
||||
*/
|
||||
export async function getAll(
|
||||
filters?: ProjectFilters,
|
||||
pagination?: PaginationParams
|
||||
pagination?: PaginationParams,
|
||||
requestingUser?: { roleName: string; projectId?: string | null; organismoOperadorId?: string | null }
|
||||
): Promise<PaginatedResult<Project>> {
|
||||
const page = pagination?.page || 1;
|
||||
const pageSize = pagination?.pageSize || 10;
|
||||
@@ -76,6 +77,17 @@ export async function getAll(
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Role-based filtering: 3-level hierarchy
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`organismo_operador_id = $${paramIndex}`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
} else if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
conditions.push(`id = $${paramIndex}`);
|
||||
params.push(requestingUser.projectId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters?.status) {
|
||||
conditions.push(`status = $${paramIndex}`);
|
||||
params.push(filters.status);
|
||||
@@ -103,7 +115,7 @@ export async function getAll(
|
||||
|
||||
// Get paginated data
|
||||
const dataQuery = `
|
||||
SELECT id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at
|
||||
SELECT id, name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by, created_at, updated_at
|
||||
FROM projects
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
@@ -131,7 +143,7 @@ export async function getAll(
|
||||
*/
|
||||
export async function getById(id: string): Promise<Project | null> {
|
||||
const result = await query<Project>(
|
||||
`SELECT id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at
|
||||
`SELECT id, name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by, created_at, updated_at
|
||||
FROM projects
|
||||
WHERE id = $1`,
|
||||
[id]
|
||||
@@ -148,9 +160,9 @@ export async function getById(id: string): Promise<Project | null> {
|
||||
*/
|
||||
export async function create(data: CreateProjectInput, userId: string): Promise<Project> {
|
||||
const result = await query<Project>(
|
||||
`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`,
|
||||
`INSERT INTO projects (name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by, created_at, updated_at`,
|
||||
[
|
||||
data.name,
|
||||
data.description || null,
|
||||
@@ -158,6 +170,7 @@ export async function create(data: CreateProjectInput, userId: string): Promise<
|
||||
data.location || null,
|
||||
data.status || 'ACTIVE',
|
||||
data.meter_type_id || null,
|
||||
data.organismo_operador_id || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
@@ -213,6 +226,12 @@ export async function update(id: string, data: UpdateProjectInput): Promise<Proj
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.organismo_operador_id !== undefined) {
|
||||
updates.push(`organismo_operador_id = $${paramIndex}`);
|
||||
params.push(data.organismo_operador_id);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Always update the updated_at timestamp
|
||||
updates.push(`updated_at = NOW()`);
|
||||
|
||||
@@ -227,7 +246,7 @@ export async function update(id: string, data: UpdateProjectInput): Promise<Proj
|
||||
`UPDATE projects
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING id, name, description, area_name, location, status, meter_type_id, created_by, created_at, updated_at`,
|
||||
RETURNING id, name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by, created_at, updated_at`,
|
||||
params
|
||||
);
|
||||
|
||||
@@ -347,7 +366,7 @@ export async function deactivateProjectAndUnassignUsers(id: string): Promise<Pro
|
||||
`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`,
|
||||
RETURNING id, name, description, area_name, location, status, meter_type_id, organismo_operador_id, created_by, created_at, updated_at`,
|
||||
[id]
|
||||
);
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ export interface CreateReadingInput {
|
||||
*/
|
||||
export async function getAll(
|
||||
filters?: ReadingFilters,
|
||||
pagination?: PaginationParams
|
||||
pagination?: PaginationParams,
|
||||
requestingUser?: { roleName: string; projectId?: string | null; organismoOperadorId?: string | null }
|
||||
): Promise<PaginatedResult<MeterReadingWithMeter>> {
|
||||
const page = pagination?.page || 1;
|
||||
const pageSize = pagination?.pageSize || 50;
|
||||
@@ -93,6 +94,17 @@ export async function getAll(
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Role-based filtering: 3-level hierarchy
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`c.project_id IN (SELECT id FROM projects WHERE organismo_operador_id = $${paramIndex})`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
} else if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
conditions.push(`c.project_id = $${paramIndex}`);
|
||||
params.push(requestingUser.projectId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters?.meter_id) {
|
||||
conditions.push(`mr.meter_id = $${paramIndex}`);
|
||||
params.push(filters.meter_id);
|
||||
@@ -246,20 +258,38 @@ export async function deleteReading(id: string): Promise<boolean> {
|
||||
* @param projectId - Optional project ID to filter
|
||||
* @returns Summary statistics
|
||||
*/
|
||||
export async function getConsumptionSummary(projectId?: string): Promise<{
|
||||
export async function getConsumptionSummary(
|
||||
projectId?: string,
|
||||
requestingUser?: { roleName: string; projectId?: string | null; organismoOperadorId?: string | null }
|
||||
): Promise<{
|
||||
totalReadings: number;
|
||||
totalMeters: number;
|
||||
avgReading: number;
|
||||
lastReadingDate: Date | null;
|
||||
}> {
|
||||
const params: unknown[] = [];
|
||||
let whereClause = '';
|
||||
const conditions: string[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (projectId) {
|
||||
whereClause = 'WHERE c.project_id = $1';
|
||||
conditions.push(`c.project_id = $${paramIndex}`);
|
||||
params.push(projectId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Role-based filtering
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`c.project_id IN (SELECT id FROM projects WHERE organismo_operador_id = $${paramIndex})`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
} else if (requestingUser && requestingUser.roleName !== 'ADMIN' && requestingUser.projectId) {
|
||||
conditions.push(`c.project_id = $${paramIndex}`);
|
||||
params.push(requestingUser.projectId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const result = await query<{
|
||||
total_readings: string;
|
||||
total_meters: string;
|
||||
|
||||
@@ -34,7 +34,8 @@ export interface PaginatedUsers {
|
||||
*/
|
||||
export async function getAll(
|
||||
filters?: UserFilter,
|
||||
pagination?: PaginationParams
|
||||
pagination?: PaginationParams,
|
||||
requestingUser?: { roleName: string; organismoOperadorId?: string | null }
|
||||
): Promise<PaginatedUsers> {
|
||||
const page = pagination?.page || 1;
|
||||
const limit = pagination?.limit || 10;
|
||||
@@ -47,6 +48,13 @@ export async function getAll(
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Role-based filtering: ORGANISMO_OPERADOR sees only users of their organismo
|
||||
if (requestingUser && requestingUser.roleName === 'ORGANISMO_OPERADOR' && requestingUser.organismoOperadorId) {
|
||||
conditions.push(`u.organismo_operador_id = $${paramIndex}`);
|
||||
params.push(requestingUser.organismoOperadorId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters?.role_id !== undefined) {
|
||||
conditions.push(`u.role_id = $${paramIndex}`);
|
||||
params.push(filters.role_id);
|
||||
@@ -83,7 +91,7 @@ export async function getAll(
|
||||
const countResult = await query<{ total: string }>(countQuery, params);
|
||||
const total = parseInt(countResult.rows[0].total, 10);
|
||||
|
||||
// Get users with role name
|
||||
// Get users with role name and organismo info
|
||||
const usersQuery = `
|
||||
SELECT
|
||||
u.id,
|
||||
@@ -94,12 +102,20 @@ export async function getAll(
|
||||
r.name as role_name,
|
||||
r.description as role_description,
|
||||
u.project_id,
|
||||
u.organismo_operador_id,
|
||||
oo.name as organismo_name,
|
||||
u.is_active,
|
||||
u.last_login,
|
||||
u.phone,
|
||||
u.street,
|
||||
u.city,
|
||||
u.state,
|
||||
u.zip_code,
|
||||
u.created_at,
|
||||
u.updated_at
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN organismos_operadores oo ON u.organismo_operador_id = oo.id
|
||||
${whereClause}
|
||||
ORDER BY u.${safeSortBy} ${safeSortOrder}
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
@@ -124,8 +140,15 @@ export async function getAll(
|
||||
}
|
||||
: undefined,
|
||||
project_id: row.project_id,
|
||||
organismo_operador_id: row.organismo_operador_id,
|
||||
organismo_name: row.organismo_name,
|
||||
is_active: row.is_active,
|
||||
last_login: row.last_login,
|
||||
phone: row.phone,
|
||||
street: row.street,
|
||||
city: row.city,
|
||||
state: row.state,
|
||||
zip_code: row.zip_code,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}));
|
||||
@@ -163,12 +186,20 @@ export async function getById(id: string): Promise<UserPublic | null> {
|
||||
r.description as role_description,
|
||||
r.permissions as role_permissions,
|
||||
u.project_id,
|
||||
u.organismo_operador_id,
|
||||
oo.name as organismo_name,
|
||||
u.is_active,
|
||||
u.last_login,
|
||||
u.phone,
|
||||
u.street,
|
||||
u.city,
|
||||
u.state,
|
||||
u.zip_code,
|
||||
u.created_at,
|
||||
u.updated_at
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN organismos_operadores oo ON u.organismo_operador_id = oo.id
|
||||
WHERE u.id = $1
|
||||
`,
|
||||
[id]
|
||||
@@ -196,8 +227,15 @@ export async function getById(id: string): Promise<UserPublic | null> {
|
||||
}
|
||||
: undefined,
|
||||
project_id: row.project_id,
|
||||
organismo_operador_id: row.organismo_operador_id,
|
||||
organismo_name: row.organismo_name,
|
||||
is_active: row.is_active,
|
||||
last_login: row.last_login,
|
||||
phone: row.phone,
|
||||
street: row.street,
|
||||
city: row.city,
|
||||
state: row.state,
|
||||
zip_code: row.zip_code,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
@@ -240,7 +278,13 @@ export async function create(data: {
|
||||
avatar_url?: string | null;
|
||||
role_id: string;
|
||||
project_id?: string | null;
|
||||
organismo_operador_id?: string | null;
|
||||
is_active?: boolean;
|
||||
phone?: string | null;
|
||||
street?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
zip_code?: string | null;
|
||||
}): Promise<UserPublic> {
|
||||
// Check if email already exists
|
||||
const existingUser = await getByEmail(data.email);
|
||||
@@ -253,9 +297,9 @@ export async function create(data: {
|
||||
|
||||
const result = await query(
|
||||
`
|
||||
INSERT INTO users (email, password_hash, name, avatar_url, role_id, project_id, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, email, name, avatar_url, role_id, project_id, is_active, last_login, created_at, updated_at
|
||||
INSERT INTO users (email, password_hash, name, avatar_url, role_id, project_id, organismo_operador_id, is_active, phone, street, city, state, zip_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING id, email, name, avatar_url, role_id, project_id, organismo_operador_id, is_active, last_login, created_at, updated_at
|
||||
`,
|
||||
[
|
||||
data.email.toLowerCase(),
|
||||
@@ -264,7 +308,13 @@ export async function create(data: {
|
||||
data.avatar_url ?? null,
|
||||
data.role_id,
|
||||
data.project_id ?? null,
|
||||
data.organismo_operador_id ?? null,
|
||||
data.is_active ?? true,
|
||||
data.phone ?? null,
|
||||
data.street ?? null,
|
||||
data.city ?? null,
|
||||
data.state ?? null,
|
||||
data.zip_code ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -288,7 +338,13 @@ export async function update(
|
||||
avatar_url?: string | null;
|
||||
role_id?: string;
|
||||
project_id?: string | null;
|
||||
organismo_operador_id?: string | null;
|
||||
is_active?: boolean;
|
||||
phone?: string | null;
|
||||
street?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
zip_code?: string | null;
|
||||
}
|
||||
): Promise<UserPublic | null> {
|
||||
// Check if user exists
|
||||
@@ -340,12 +396,48 @@ export async function update(
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.organismo_operador_id !== undefined) {
|
||||
updates.push(`organismo_operador_id = $${paramIndex}`);
|
||||
params.push(data.organismo_operador_id);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.is_active !== undefined) {
|
||||
updates.push(`is_active = $${paramIndex}`);
|
||||
params.push(data.is_active);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.phone !== undefined) {
|
||||
updates.push(`phone = $${paramIndex}`);
|
||||
params.push(data.phone);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.street !== undefined) {
|
||||
updates.push(`street = $${paramIndex}`);
|
||||
params.push(data.street);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.city !== undefined) {
|
||||
updates.push(`city = $${paramIndex}`);
|
||||
params.push(data.city);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.state !== undefined) {
|
||||
updates.push(`state = $${paramIndex}`);
|
||||
params.push(data.state);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (data.zip_code !== undefined) {
|
||||
updates.push(`zip_code = $${paramIndex}`);
|
||||
params.push(data.zip_code);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
@@ -45,8 +45,15 @@ export interface UserPublic {
|
||||
role_id: string;
|
||||
role?: Role;
|
||||
project_id: string | null;
|
||||
organismo_operador_id?: string | null;
|
||||
organismo_name?: string | null;
|
||||
is_active: boolean;
|
||||
last_login: Date | null;
|
||||
phone?: string | null;
|
||||
street?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
zip_code?: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -57,10 +64,23 @@ export interface JwtPayload {
|
||||
roleId: string;
|
||||
roleName: string;
|
||||
projectId?: string | null;
|
||||
organismoOperadorId?: string | null;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
export interface OrganismoOperador {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
region: string | null;
|
||||
contact_name: string | null;
|
||||
contact_email: string | null;
|
||||
is_active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: JwtPayload;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import jwt, { SignOptions, VerifyOptions } from 'jsonwebtoken';
|
||||
import config from '../config';
|
||||
import logger from './logger';
|
||||
import type { JwtPayload } from '../types';
|
||||
|
||||
interface TokenPayload {
|
||||
userId?: string;
|
||||
email?: string;
|
||||
roleId?: string;
|
||||
roleName?: string;
|
||||
projectId?: string | null;
|
||||
organismoOperadorId?: string | null;
|
||||
id?: string;
|
||||
role?: string;
|
||||
[key: string]: unknown;
|
||||
|
||||
33
water-api/src/utils/scope.ts
Normal file
33
water-api/src/utils/scope.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { query } from '../config/database';
|
||||
|
||||
interface ScopeUser {
|
||||
roleName: string;
|
||||
projectId?: string | null;
|
||||
organismoOperadorId?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed project IDs for a user based on their role hierarchy.
|
||||
* - ADMIN: returns null (all projects)
|
||||
* - ORGANISMO_OPERADOR: returns project IDs belonging to their organismo
|
||||
* - OPERADOR/OPERATOR: returns their single project_id
|
||||
*/
|
||||
export async function getAllowedProjectIds(user: ScopeUser): Promise<string[] | null> {
|
||||
if (user.roleName === 'ADMIN') {
|
||||
return null; // No restriction
|
||||
}
|
||||
|
||||
if (user.roleName === 'ORGANISMO_OPERADOR' && user.organismoOperadorId) {
|
||||
const result = await query<{ id: string }>(
|
||||
'SELECT id FROM projects WHERE organismo_operador_id = $1',
|
||||
[user.organismoOperadorId]
|
||||
);
|
||||
return result.rows.map(r => r.id);
|
||||
}
|
||||
|
||||
if (user.projectId) {
|
||||
return [user.projectId];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -131,6 +131,11 @@ export const createMeterSchema = z.object({
|
||||
|
||||
// Additional Data
|
||||
data: z.record(z.any()).optional().nullable(),
|
||||
|
||||
// Address & Account Fields
|
||||
address: z.string().optional().nullable(),
|
||||
cespt_account: z.string().max(50).optional().nullable(),
|
||||
cadastral_key: z.string().max(50).optional().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -233,6 +238,11 @@ export const updateMeterSchema = z.object({
|
||||
|
||||
// Additional Data
|
||||
data: z.record(z.any()).optional().nullable(),
|
||||
|
||||
// Address & Account Fields
|
||||
address: z.string().optional().nullable(),
|
||||
cespt_account: z.string().max(50).optional().nullable(),
|
||||
cadastral_key: z.string().max(50).optional().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,11 @@ export const createProjectSchema = z.object({
|
||||
.uuid('Meter type ID must be a valid UUID')
|
||||
.optional()
|
||||
.nullable(),
|
||||
organismo_operador_id: z
|
||||
.string()
|
||||
.uuid('Organismo operador ID must be a valid UUID')
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -84,6 +89,11 @@ export const updateProjectSchema = z.object({
|
||||
.uuid('Meter type ID must be a valid UUID')
|
||||
.optional()
|
||||
.nullable(),
|
||||
organismo_operador_id: z
|
||||
.string()
|
||||
.uuid('Organismo operador ID must be a valid UUID')
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,17 @@ export const createUserSchema = z.object({
|
||||
.uuid('Project ID must be a valid UUID')
|
||||
.nullable()
|
||||
.optional(),
|
||||
organismo_operador_id: z
|
||||
.string()
|
||||
.uuid('Organismo Operador ID must be a valid UUID')
|
||||
.nullable()
|
||||
.optional(),
|
||||
is_active: z.boolean().default(true),
|
||||
phone: z.string().max(20).optional().nullable(),
|
||||
street: z.string().max(255).optional().nullable(),
|
||||
city: z.string().max(100).optional().nullable(),
|
||||
state: z.string().max(100).optional().nullable(),
|
||||
zip_code: z.string().max(10).optional().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -68,7 +78,17 @@ export const updateUserSchema = z.object({
|
||||
.uuid('Project ID must be a valid UUID')
|
||||
.nullable()
|
||||
.optional(),
|
||||
organismo_operador_id: z
|
||||
.string()
|
||||
.uuid('Organismo Operador ID must be a valid UUID')
|
||||
.nullable()
|
||||
.optional(),
|
||||
is_active: z.boolean().optional(),
|
||||
phone: z.string().max(20).optional().nullable(),
|
||||
street: z.string().max(255).optional().nullable(),
|
||||
city: z.string().max(100).optional().nullable(),
|
||||
state: z.string().max(100).optional().nullable(),
|
||||
zip_code: z.string().max(10).optional().nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
Reference in New Issue
Block a user