Intranet Empresarial

Guía Completa de Implementación

Versión: 1.0

Fecha: Abril 2026

Stack: .NET 8 + SQL Server + SFTP

Integración: Profit ERP

1. Resumen Ejecutivo

Esta guía describe la implementación completa de una Intranet Empresarial desarrollada con tecnologías Microsoft (.NET 8 + SQL Server), integrada con Profit ERP para datos de empleados y utilizando SFTP para gestión documental.

Alcance del Proyecto:
Módulos: Autenticación, Drive, RRHH (Vacaciones, Permisos, Nómina), Chat, Organigrama, Capacitación, Normativas.
ParámetroValor
Duración3 meses (12-13 semanas)
Inversión$8,000 - $10,000 USD
Equipo1 Desarrollador Senior Full Stack
Capacidad500+ usuarios concurrentes
Base de DatosSQL Server 2019+ (existente)

2. Arquitectura del Sistema

2.1 Diagrama de Componentes

Frontend
HTML + Tailwind
API .NET 8
REST + SignalR
SQL Server
Datos + Chat


API .NET 8
SFTP Server
Archivos


API .NET 8
Profit ERP
Empleados

2.2 Flujo de Datos

  1. Autenticación: Usuario → API valida SQL Server → JWT → Acceso
  2. Empleados: API consulta vista Profit → Cache → Frontend
  3. Archivos: Upload → API → SFTP → Metadatos SQL
  4. Chat: Mensaje → SignalR → SQL Server → Broadcast

3. Estructura de la Solución .NET

3.1 Organización de Proyectos

IntranetEmpresarial/ ├── src/ │ ├── Intranet.Web/ # Proyecto Principal (MVC) │ │ ├── Controllers/ # Controladores API │ │ ├── Views/ # Vistas Razor │ │ ├── wwwroot/ # Assets estáticos │ │ ├── Hubs/ # SignalR Hubs │ │ └── Program.cs │ │ │ ├── Intranet.Core/ # Capa de Dominio │ │ ├── Entities/ # Entidades │ │ ├── Interfaces/ # Repositorios │ │ └── Services/ # Lógica de negocio │ │ │ ├── Intranet.Infrastructure/ # Capa de Infraestructura │ │ ├── Data/ # DbContext EF Core │ │ ├── Repositories/ # Implementaciones │ │ ├── Services/ # Servicios externos │ │ └── Sftp/ # Cliente SFTP │ │ │ └── Intranet.Integration/ # Integraciones │ ├── Profit/ # Conector Profit ERP │ └── Notifications/ # Email/Push │ ├── tests/ │ ├── Intranet.UnitTests/ │ └── Intranet.IntegrationTests/ │ └── Intranet.sln

3.2 Capas de la Arquitectura

CapaResponsabilidadEjemplos
WebControladores, Views, HubsAuthController, ChatHub
CoreEntidades, Interfaces, LógicaUser, IVacationRepository
InfrastructureEF Core, Repositorios, SFTPAppDbContext, SftpService
IntegrationConectores externosProfitConnector

4. Esquema SQL Server Completo

IMPORTANTE: No duplicar datos de empleados. Usar vista de Profit ERP.

4.1 Vista de Profit (Lectura)

-- Crear Linked Server a Profit (ejecutar una vez) EXEC sp_addlinkedserver @server = 'PROFIT_SERVER', @srvproduct = 'SQL Server'; -- Vista de empleados desde Profit CREATE VIEW vw_Profit_Employees AS SELECT employee_code, first_name, last_name, email, department_code, position, hire_date, status, supervisor_code FROM PROFIT_SERVER.ProfitDB.dbo.Employees; -- Crear vista de departamentos CREATE VIEW vw_Profit_Departments AS SELECT department_code, department_name, cost_center FROM PROFIT_SERVER.ProfitDB.dbo.Departments;

4.2 Tablas Intranet (Datos Propios)

-- Extensión de empleados (datos adicionales) CREATE TABLE Employee_Extensions ( id INT IDENTITY(1,1) PRIMARY KEY, employee_code VARCHAR(50) NOT NULL, avatar_url NVARCHAR(500), phone_extension VARCHAR(20), mobile VARCHAR(50), bio NVARCHAR(MAX), skills NVARCHAR(MAX), -- JSON two_factor_enabled BIT DEFAULT 0, notification_preferences NVARCHAR(MAX), -- JSON created_at DATETIME2 DEFAULT GETDATE(), FOREIGN KEY (employee_code) REFERENCES vw_Profit_Employees(employee_code) ); -- Usuarios y credenciales CREATE TABLE users_credentials ( id INT IDENTITY(1,1) PRIMARY KEY, employee_code VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, is_active BIT DEFAULT 1, last_login_at DATETIME2, two_factor_enabled BIT DEFAULT 0, created_at DATETIME2 DEFAULT GETDATE() ); -- Archivos (metadatos) CREATE TABLE file_system_items ( id INT IDENTITY(1,1) PRIMARY KEY, file_id UNIQUEIDENTIFIER DEFAULT NEWID(), parent_id INT NULL, name NVARCHAR(500) NOT NULL, item_type VARCHAR(20) CHECK (item_type IN ('folder', 'file')), file_size BIGINT, sftp_path NVARCHAR(1000), owner_code VARCHAR(50) NOT NULL, is_shared BIT DEFAULT 0, created_at DATETIME2 DEFAULT GETDATE(), FOREIGN KEY (parent_id) REFERENCES file_system_items(id) ON DELETE CASCADE ); -- Chat - Conversaciones CREATE TABLE conversations ( id INT IDENTITY(1,1) PRIMARY KEY, conversation_type VARCHAR(20) CHECK (conversation_type IN ('direct', 'group')), title NVARCHAR(255), created_by VARCHAR(50) NOT NULL, created_at DATETIME2 DEFAULT GETDATE() ); -- Chat - Mensajes (con purga automática) CREATE TABLE messages ( id INT IDENTITY(1,1) PRIMARY KEY, conversation_id INT NOT NULL, sender_code VARCHAR(50) NOT NULL, content NVARCHAR(MAX), sent_at DATETIME2 DEFAULT GETDATE(), purge_date AS (DATEADD(year, 1, sent_at)) PERSISTED, FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE ); CREATE INDEX IX_Messages_Purge ON messages(purge_date); -- Vacaciones CREATE TABLE vacation_requests ( id INT IDENTITY(1,1) PRIMARY KEY, employee_code VARCHAR(50) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, total_days INT NOT NULL, reason NVARCHAR(MAX), status VARCHAR(50) DEFAULT 'pending', supervisor_code VARCHAR(50), approved_at DATETIME2, requested_at DATETIME2 DEFAULT GETDATE() ); -- Nómina (extension de Profit) CREATE TABLE payroll_receipts ( id INT IDENTITY(1,1) PRIMARY KEY, employee_code VARCHAR(50) NOT NULL, period_year INT NOT NULL, period_month INT NOT NULL, net_salary DECIMAL(12,2) NOT NULL, receipt_file_path NVARCHAR(1000), is_paid BIT DEFAULT 0, created_at DATETIME2 DEFAULT GETDATE(), UNIQUE(employee_code, period_year, period_month) ); -- Cursos y capacitación CREATE TABLE courses ( id INT IDENTITY(1,1) PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, title NVARCHAR(255) NOT NULL, description NVARCHAR(MAX), duration_minutes INT, instructor_code VARCHAR(50), is_published BIT DEFAULT 0, created_at DATETIME2 DEFAULT GETDATE() ); -- Inscripciones a cursos CREATE TABLE course_enrollments ( id INT IDENTITY(1,1) PRIMARY KEY, course_id INT NOT NULL, employee_code VARCHAR(50) NOT NULL, enrollment_date DATETIME2 DEFAULT GETDATE(), completion_percentage DECIMAL(5,2) DEFAULT 0, completed_at DATETIME2, FOREIGN KEY (course_id) REFERENCES courses(id), UNIQUE(course_id, employee_code) ); -- Normativas CREATE TABLE policies ( id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(500) NOT NULL, content NVARCHAR(MAX) NOT NULL, version VARCHAR(20) NOT NULL, effective_date DATE NOT NULL, is_mandatory BIT DEFAULT 1, created_by VARCHAR(50) NOT NULL, created_at DATETIME2 DEFAULT GETDATE() ); -- Reconocimiento de normativas CREATE TABLE policy_acknowledgments ( id INT IDENTITY(1,1) PRIMARY KEY, policy_id INT NOT NULL, employee_code VARCHAR(50) NOT NULL, is_read BIT DEFAULT 0, is_acknowledged BIT DEFAULT 0, acknowledged_at DATETIME2, FOREIGN KEY (policy_id) REFERENCES policies(id), UNIQUE(policy_id, employee_code) );

4.3 Job de Purga Automática (Mensajes > 1 año)

-- Stored Procedure para purga CREATE PROCEDURE sp_PurgeOldMessages AS BEGIN SET NOCOUNT ON; DECLARE @BatchSize INT = 1000; DECLARE @RowsDeleted INT = 1; WHILE @RowsDeleted > 0 BEGIN DELETE TOP (@BatchSize) FROM messages WHERE sent_at < DATEADD(year, -1, GETDATE()); SET @RowsDeleted = @@ROWCOUNT; IF @RowsDeleted > 0 WAITFOR DELAY '00:00:01'; END END; -- Crear Job SQL Agent (ejecutar diariamente a las 3 AM) EXEC sp_add_job @job_name = N'Purge Old Chat Messages', @enabled = 1; EXEC sp_add_jobstep @job_name = N'Purge Old Chat Messages', @step_name = N'Execute Purge', @subsystem = N'TSQL', @command = N'EXEC sp_PurgeOldMessages'; EXEC sp_add_schedule @schedule_name = N'Daily at 3AM', @freq_type = 4, -- Daily @freq_interval = 1, @active_start_time = 030000; -- 3:00 AM EXEC sp_attach_schedule @job_name = N'Purge Old Chat Messages', @schedule_name = N'Daily at 3AM'; EXEC sp_add_jobserver @job_name = N'Purge Old Chat Messages';

5. APIs y Endpoints Detallados

5.1 Autenticación (/api/auth)

POST /api/auth/login

Request:
{ "email": "usuario@empresa.com", "password": "********", "rememberMe": true }
Response 200:
{ "token": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g...", "expiresAt": "2024-04-23T18:30:00Z", "user": { "employeeCode": "EMP001", "name": "Juan Pérez", "role": "employee", "department": "IT" } }
POST /api/auth/refresh

Request:
{ "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g..." }

5.2 Archivos (/api/files)

GET /api/files?parentId={id}

Response: Lista de archivos/carpetas
POST /api/files/upload

Content-Type: multipart/form-data
Proceso:
  1. Cliente sube archivo
  2. API guarda en SFTP
  3. API guarda metadatos en SQL
  4. Retorna URL de acceso
GET /api/files/{id}/download

Response: Redirect a URL temporal SFTP o stream directo

5.3 Chat (/api/chat)

GET /api/chat/conversations
Lista de conversaciones del usuario autenticado
GET /api/chat/conversations/{id}/messages
Query params: page, pageSize, beforeId
POST /api/chat/conversations/{id}/messages

WebSocket alternativo: SignalR Hub /chatHub

5.4 RRHH - Vacaciones (/api/vacations)

GET /api/vacations/balance
Saldo de vacaciones del usuario actual
POST /api/vacations/request

{ "startDate": "2024-05-15", "endDate": "2024-05-20", "totalDays": 5, "reason": "Vacaciones familiares" }
PUT /api/vacations/{id}/approve
Aprobar/rechazar solicitud (solo supervisors)

5.5 Nómina (/api/payroll)

GET /api/payroll/receipts
Lista de recibos de pago del usuario
GET /api/payroll/receipts/{id}/download
Descarga PDF del recibo (generado desde datos Profit + template)

5.6 Organigrama (/api/orgchart)

GET /api/orgchart
Response: Estructura jerárquica completa desde Profit
GET /api/orgchart/{employeeCode}/team
Equipo directo de un empleado (subordinados)
GET /api/orgchart/{employeeCode}/hierarchy
Cadena de mando (superiores hasta CEO)

5.7 Capacitación (/api/training)

GET /api/training/courses
Lista de cursos disponibles
POST /api/training/courses/{id}/enroll
Inscribirse a curso
PUT /api/training/progress
Actualizar progreso (% completado)
GET /api/training/certificates/{id}/download
Descargar certificado en PDF

5.8 Normativas (/api/policies)

GET /api/policies
Lista de políticas y normativas
POST /api/policies/{id}/acknowledge
Confirmar lectura (firma digital simple)

6. Integración Profit ERP

6.1 Métodos de Integración

MétodoVentajasDesventajasRecomendado
Linked ServerDirecto, rápidoRequiere permisos DB✅ Sí
API REST ProfitSeguro, estándarLatencia, rate limitsSi tiene API
ReplicationDatos localesComplejo, delayNo recomendado

6.2 Implementación Linked Server

-- Paso 1: Crear Linked Server EXEC sp_addlinkedserver @server = 'PROFIT_SERVER', @srvproduct = '', @provider = 'SQLOLEDB', @datasrc = '192.168.1.100', -- IP servidor Profit @catalog = 'ProfitDB'; -- Paso 2: Configurar seguridad EXEC sp_addlinkedsrvlogin @rmtsrvname = 'PROFIT_SERVER', @useself = 'FALSE', @locallogin = NULL, @rmtuser = 'intranet_user', @rmtpassword = 'password_seguro'; -- Paso 3: Probar conexión SELECT * FROM OPENQUERY(PROFIT_SERVER, 'SELECT COUNT(*) FROM Employees'); -- Paso 4: Crear vistas locales CREATE VIEW vw_Profit_Employees AS SELECT e.employee_code, e.first_name, e.last_name, e.email, e.department_code, d.department_name, e.position, e.hire_date, e.status, e.supervisor_code FROM PROFIT_SERVER.ProfitDB.dbo.Employees e LEFT JOIN PROFIT_SERVER.ProfitDB.dbo.Departments d ON e.department_code = d.department_code WHERE e.status = 'active'; -- Paso 5: Crear sinónimos (opcional) CREATE SYNONYM Profit_Employees FOR PROFIT_SERVER.ProfitDB.dbo.Employees;

6.3 Cache de Datos Profit

Para evitar saturar el servidor Profit:

// Servicio de cache para empleados public class ProfitEmployeeCacheService { private readonly IMemoryCache _cache; private readonly ApplicationDbContext _context; public async Task GetEmployeeAsync(string employeeCode) { // Cache por 5 minutos return await _cache.GetOrCreateAsync( $"emp_{employeeCode}", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await _context.Employees .FromSqlRaw("SELECT * FROM vw_Profit_Employees WHERE employee_code = {0}", employeeCode) .FirstOrDefaultAsync(); }); } // Invalidar cache cuando cambian datos public void InvalidateCache(string employeeCode) { _cache.Remove($"emp_{employeeCode}"); } }

6.4 Sincronización Programada

-- Job para sincronizar cambios cada 15 minutos CREATE PROCEDURE sp_SyncProfitEmployees AS BEGIN -- Actualizar extensión de empleados nuevos INSERT INTO Employee_Extensions (employee_code) SELECT e.employee_code FROM vw_Profit_Employees e LEFT JOIN Employee_Extensions ex ON e.employee_code = ex.employee_code WHERE ex.employee_code IS NULL; -- Desactivar usuarios de empleados dados de baja UPDATE users_credentials SET is_active = 0 WHERE employee_code NOT IN ( SELECT employee_code FROM vw_Profit_Employees WHERE status = 'active' ); END; -- Programar cada 15 minutos EXEC sp_add_job @job_name = 'Sync Profit Employees'; EXEC sp_add_jobstep @job_name = 'Sync Profit Employees', @step_name = 'Sync', @command = 'EXEC sp_SyncProfitEmployees'; EXEC sp_add_schedule @schedule_name = 'Every15Min', @freq_type = 4, @freq_interval = 1, @freq_subday_type = 4, @freq_subday_interval = 15; EXEC sp_attach_schedule @job_name = 'Sync Profit Employees', @schedule_name = 'Every15Min';

7. Configuración SFTP

7.1 Estructura de Carpetas

/intranet/ ├── files/ │ ├── 2024/ │ │ ├── 01/ │ │ ├── 02/ │ │ └── ... │ └── 2025/ ├── temp/ │ └── uploads/ # Archivos temporales durante upload ├── backups/ │ └── daily/ # Backups automáticos └── system/ └── avatars/ # Fotos de perfil

7.2 Implementación Cliente SFTP (.NET)

using SSH.NET; public class SftpService { private readonly string _host; private readonly string _username; private readonly string _password; private readonly string _basePath; public async Task UploadFileAsync(Stream fileStream, string fileName) { using var sftp = new SftpClient(_host, _username, _password); await sftp.ConnectAsync(); var year = DateTime.Now.Year.ToString(); var month = DateTime.Now.Month.ToString("D2"); var folderPath = $"{_basePath}/files/{year}/{month}"; // Crear directorio si no existe if (!sftp.Exists(folderPath)) sftp.CreateDirectory(folderPath); var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; var fullPath = $"{folderPath}/{uniqueFileName}"; await sftp.UploadFileAsync(fileStream, fullPath); return fullPath; } public async Task DownloadFileAsync(string filePath) { using var sftp = new SftpClient(_host, _username, _password); await sftp.ConnectAsync(); var memoryStream = new MemoryStream(); await sftp.DownloadFileAsync(filePath, memoryStream); memoryStream.Position = 0; return memoryStream; } public async Task DeleteFileAsync(string filePath) { using var sftp = new SftpClient(_host, _username, _password); await sftp.ConnectAsync(); if (sftp.Exists(filePath)) sftp.DeleteFile(filePath); } }

7.3 Configuración appsettings.json

{ "Sftp": { "Host": "sftp.empresa.com", "Port": 22, "Username": "intranet_service", "Password": "${SFTP_PASSWORD}", "BasePath": "/intranet/files", "MaxFileSizeMB": 100 }, "Profit": { "Server": "PROFIT_SERVER", "CacheMinutes": 5 } }

8. Guía de Implementación Paso a Paso

Fase 1: Setup Inicial (Semana 1)

1 Crear proyecto .NET 8
dotnet new mvc -n Intranet.Web
dotnet new classlib -n Intranet.Core
dotnet new classlib -n Intranet.Infrastructure
2 Configurar EF Core con SQL Server
Instalar paquetes: Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools
3 Configurar Linked Server a Profit
Ejecutar scripts SQL de sección 6.2
4 Crear tablas de base de datos
Ejecutar scripts de sección 4
5 Configurar conexión SFTP
Instalar SSH.NET, probar conexión

Fase 2: Autenticación (Semana 2)

6 Implementar JWT Authentication
Configurar AddAuthentication().AddJwtBearer()
7 Crear endpoints /api/auth/login y /api/auth/refresh
8 Integrar login con datos de Profit
Validar usuario contra vista vw_Profit_Employees
9 Crear middleware de autorización por roles

Fase 3: Drive / Archivos (Semanas 3-4)

10 Implementar SftpService
Upload, download, delete, crear carpetas
11 Crear FileController
Endpoints: listar, subir, descargar, compartir
12 Conectar frontend existente a API
Adaptar drive.html para usar endpoints reales

Fase 4: RRHH (Semanas 5-7)

13 Vacaciones: modelo y endpoints
Solicitud, aprobación, calendario, saldos
14 Permisos: flujo de aprobación
Con notificaciones por email
15 Nómina: integrar con Profit
Leer datos de nómina, generar PDFs de recibos
16 Fideicomiso y Beneficios
Estados de cuenta, catálogo de beneficios

Fase 5: Chat (Semanas 8-9)

17 Configurar SignalR Hub
app.MapHub("/chatHub");
18 Implementar mensajería en tiempo real
Enviar/recibir mensajes, indicadores de escritura
19 Adjuntos en chat (integración con Drive)
20 Configurar job de purga de mensajes
SQL Agent Job diario (sección 4.3)

Fase 6: Organigrama y Otros (Semana 10)

21 Organigrama desde Profit
API que retorna jerarquía completa
22 Capacitación: cursos y progreso
23 Normativas: lectura y firmas

Fase 7: Testing y Deploy (Semanas 11-13)

24 Testing unitario e integración
xUnit, pruebas de APIs
25 Configurar CI/CD
GitHub Actions o Azure DevOps
26 Deploy en servidor de producción
IIS o Docker + Reverse Proxy
27 Configurar SSL y dominio
HTTPS, redirección www
28 Capacitación de usuarios y entrega

9. Seguridad y Buenas Prácticas

9.1 Autenticación y Autorización

9.2 Datos Sensibles

9.3 SFTP

9.4 Headers de Seguridad

// En Program.cs app.Use(async (context, next) => { context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin"); await next(); });

9.5 Backup y Recuperación

10. Checklist de Entrega

Desarrollo

Calidad

Documentación

Deploy

Entrega

¡Proyecto Completado!
La intranet está lista para uso productivo.