feat(fsm): implementar gestion y asignacion de sets de herramientas a tecnicos
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# Ideas y Propuestas — Help Desk
|
# Ideas y Propuestas — Help Desk
|
||||||
|
|
||||||
|
## Ideas Implementadas
|
||||||
|
|
||||||
|
1. **Gestión y Asignación de Sets de Herramientas a Técnicos (FSM):**
|
||||||
|
- **Estado:** ✅ Implementado y desplegado.
|
||||||
|
- **Modelos:** `Herramienta`, `HerramientaSet`, `HerramientaAsignacion`.
|
||||||
|
- **Alcance:**
|
||||||
|
- Catálogo detallado de herramientas con código interno, número de serie, marca, modelo, estado operativo y ubicación en bodega.
|
||||||
|
- Agrupación en Sets / Kits especializados (ej. Kit Fibra Óptica, Kit CCTV Seguridad, Kit Redes).
|
||||||
|
- Asignación mixta: Dotación fija (custodia permanente del técnico) o asignación por Visita presencial de campo (FSM) vinculada a la orden de trabajo.
|
||||||
|
- Interfaz administrativa (`/admin/herramientas`) con pestañas y modales de gestión, y vista del técnico (`/agent/herramientas`) para ver su equipamiento en custodia.
|
||||||
|
- Endpoint M2M `GET /api/external/herramientas/sets` para consulta de kits por servicios externos.
|
||||||
|
|
||||||
## Ideas Pendientes de Evaluación
|
## Ideas Pendientes de Evaluación
|
||||||
|
|
||||||
1. **Canalización M2M de Tickets a `app_ideas` (Canvas Estratégico):**
|
1. **Canalización M2M de Tickets a `app_ideas` (Canvas Estratégico):**
|
||||||
|
|||||||
+148
@@ -155,6 +155,8 @@ async function setup() {
|
|||||||
estado ENUM('programada', 'en_camino', 'en_sitio', 'completada', 'cancelada') DEFAULT 'programada',
|
estado ENUM('programada', 'en_camino', 'en_sitio', 'completada', 'cancelada') DEFAULT 'programada',
|
||||||
checklist_json TEXT NULL,
|
checklist_json TEXT NULL,
|
||||||
historial_json LONGTEXT NULL,
|
historial_json LONGTEXT NULL,
|
||||||
|
set_herramientas_id INT NULL,
|
||||||
|
set_herramientas_nombre VARCHAR(150) NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
INDEX (ticket_id),
|
INDEX (ticket_id),
|
||||||
@@ -165,6 +167,152 @@ async function setup() {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await migrateColumn('ticket_visitas', 'set_herramientas_id', 'set_herramientas_id INT NULL');
|
||||||
|
await migrateColumn('ticket_visitas', 'set_herramientas_nombre', 'set_herramientas_nombre VARCHAR(150) NULL');
|
||||||
|
|
||||||
|
// Create herramientas (Catálogo detallado de herramientas físicas e instrumentos)
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS herramientas (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
nombre VARCHAR(150) NOT NULL,
|
||||||
|
codigo VARCHAR(50) NULL,
|
||||||
|
numero_serie VARCHAR(100) NULL,
|
||||||
|
categoria VARCHAR(100) DEFAULT 'general',
|
||||||
|
marca VARCHAR(100) NULL,
|
||||||
|
modelo VARCHAR(100) NULL,
|
||||||
|
estado ENUM('operativo', 'en_mantenimiento', 'danado', 'baja') DEFAULT 'operativo',
|
||||||
|
ubicacion_bodega VARCHAR(150) NULL,
|
||||||
|
notas TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX (company_id),
|
||||||
|
INDEX (codigo),
|
||||||
|
INDEX (estado),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create herramientas_sets (Agrupaciones lógicas / Kits de herramientas reutilizables)
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS herramientas_sets (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
nombre VARCHAR(150) NOT NULL,
|
||||||
|
codigo VARCHAR(50) NULL,
|
||||||
|
descripcion TEXT NULL,
|
||||||
|
categoria VARCHAR(100) DEFAULT 'general',
|
||||||
|
is_active TINYINT(1) DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX (company_id),
|
||||||
|
INDEX (codigo),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create herramientas_set_items (Herramientas incluidas en cada Set/Kit)
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS herramientas_set_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
set_id INT NOT NULL,
|
||||||
|
herramienta_id INT NOT NULL,
|
||||||
|
cantidad INT DEFAULT 1,
|
||||||
|
FOREIGN KEY (set_id) REFERENCES herramientas_sets(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (herramienta_id) REFERENCES herramientas(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY idx_set_herramienta (set_id, herramienta_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create tecnico_herramientas_asignaciones (Asignación mixta: Dotación fija o por Visita FSM)
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tecnico_herramientas_asignaciones (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
tecnico_id INT NOT NULL,
|
||||||
|
set_id INT NULL,
|
||||||
|
herramienta_id INT NULL,
|
||||||
|
ticket_visita_id INT NULL,
|
||||||
|
tipo_asignacion ENUM('dotacion_fija', 'visita_fsm') DEFAULT 'dotacion_fija',
|
||||||
|
fecha_asignacion DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
fecha_devolucion DATETIME NULL,
|
||||||
|
estado ENUM('asignado', 'devuelto', 'observado') DEFAULT 'asignado',
|
||||||
|
observaciones_entrega TEXT NULL,
|
||||||
|
observaciones_devolucion TEXT NULL,
|
||||||
|
asignado_por INT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX (company_id),
|
||||||
|
INDEX (tecnico_id),
|
||||||
|
INDEX (ticket_visita_id),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (tecnico_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (set_id) REFERENCES herramientas_sets(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (herramienta_id) REFERENCES herramientas(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (ticket_visita_id) REFERENCES ticket_visitas(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (asignado_por) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Seed herramientas y sets iniciales para Telcotronics si no existen
|
||||||
|
const [existingTools] = await conn.query('SELECT COUNT(*) as c FROM herramientas WHERE company_id = 1');
|
||||||
|
if (existingTools[0].c === 0) {
|
||||||
|
console.log('+ Sembrando catálogo de herramientas para Telcotronics...');
|
||||||
|
const initialTools = [
|
||||||
|
[1, 'Fusionadora de Fibra Óptica', 'TOOL-FIB-01', 'SN-FIB7021', 'fibra_optica', 'Fujikura', '70S', 'operativo', 'Bodega Central - Estante A1'],
|
||||||
|
[1, 'OTDR Mini Pro de Precisión', 'TOOL-OTDR-01', 'SN-OTDR4402', 'medicion', 'Anritsu', 'MT9090', 'operativo', 'Bodega Central - Estante A1'],
|
||||||
|
[1, 'Crimpadora RJ45/RJ11 Pro', 'TOOL-CRIMP-01', 'SN-CRIMP-88', 'redes', 'Klein Tools', 'VDV226', 'operativo', 'Bodega Central - Caja 3'],
|
||||||
|
[1, 'Tester Certificador Cable de Red PoE', 'TOOL-TEST-01', 'SN-FLK8891', 'medicion', 'Fluke Networks', 'MicroScanner', 'operativo', 'Bodega Central - Estante B2'],
|
||||||
|
[1, 'Monitor Portátil de Prueba CCTV AHD/IP', 'TOOL-CCTV-01', 'SN-HK9012', 'cctv', 'Hikvision', 'TestScreen Pro 4K', 'operativo', 'Bodega Central - Estante C1'],
|
||||||
|
[1, 'Taladro Percutor Inalámbrico 20V Max', 'TOOL-TAL-01', 'SN-DW2019', 'mano', 'DeWalt', 'DCD778', 'operativo', 'Bodega Central - Gaveta 5'],
|
||||||
|
[1, 'Multímetro Digital True RMS', 'TOOL-MULT-01', 'SN-FLK1174', 'electricidad', 'Fluke', '117 Electrician', 'operativo', 'Bodega Central - Estante B2'],
|
||||||
|
[1, 'Escalera Telescópica de Aluminio 3.8m', 'TOOL-ESC-01', 'SN-ESC-901', 'mano', 'Werner', 'Telescop 3.8', 'operativo', 'Área de Vehículos']
|
||||||
|
];
|
||||||
|
for (const t of initialTools) {
|
||||||
|
await conn.query(
|
||||||
|
`INSERT INTO herramientas (company_id, nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear sets
|
||||||
|
console.log('+ Sembrando Sets / Kits de Herramientas para Telcotronics...');
|
||||||
|
const [set1] = await conn.query(
|
||||||
|
`INSERT INTO herramientas_sets (company_id, nombre, codigo, descripcion, categoria)
|
||||||
|
VALUES (1, 'Kit Fibra Óptica & Enlaces', 'SET-FIB-PRO', 'Kit completo para tendido, fusión y certificación de enlaces de fibra óptica.', 'fibra_optica')`
|
||||||
|
);
|
||||||
|
const [set2] = await conn.query(
|
||||||
|
`INSERT INTO herramientas_sets (company_id, nombre, codigo, descripcion, categoria)
|
||||||
|
VALUES (1, 'Kit Seguridad Electrónica & CCTV', 'SET-CCTV-01', 'Herramientas indispensables para montaje, alineación y pruebas de cámaras IP y analógicas.', 'cctv')`
|
||||||
|
);
|
||||||
|
const [set3] = await conn.query(
|
||||||
|
`INSERT INTO herramientas_sets (company_id, nombre, codigo, descripcion, categoria)
|
||||||
|
VALUES (1, 'Kit Redes & Cableado Estructurado', 'SET-RED-01', 'Instrumentos para certificación de puntos de red, crimpado y pruebas PoE.', 'redes')`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Asociar herramientas a los sets
|
||||||
|
const [toolsRows] = await conn.query('SELECT id, codigo FROM herramientas WHERE company_id = 1');
|
||||||
|
const toolMap = {};
|
||||||
|
toolsRows.forEach(r => { toolMap[r.codigo] = r.id; });
|
||||||
|
|
||||||
|
// Set 1 items: Fusionadora + OTDR + Crimpadora
|
||||||
|
if (toolMap['TOOL-FIB-01'] && toolMap['TOOL-OTDR-01']) {
|
||||||
|
await conn.query('INSERT INTO herramientas_set_items (set_id, herramienta_id, cantidad) VALUES (?, ?, 1), (?, ?, 1)',
|
||||||
|
[set1.insertId, toolMap['TOOL-FIB-01'], set1.insertId, toolMap['TOOL-OTDR-01']]);
|
||||||
|
}
|
||||||
|
// Set 2 items: Monitor CCTV + Taladro + Multímetro
|
||||||
|
if (toolMap['TOOL-CCTV-01'] && toolMap['TOOL-TAL-01'] && toolMap['TOOL-MULT-01']) {
|
||||||
|
await conn.query('INSERT INTO herramientas_set_items (set_id, herramienta_id, cantidad) VALUES (?, ?, 1), (?, ?, 1), (?, ?, 1)',
|
||||||
|
[set2.insertId, toolMap['TOOL-CCTV-01'], set2.insertId, toolMap['TOOL-TAL-01'], set2.insertId, toolMap['TOOL-MULT-01']]);
|
||||||
|
}
|
||||||
|
// Set 3 items: Tester Fluke + Crimpadora + Multímetro
|
||||||
|
if (toolMap['TOOL-TEST-01'] && toolMap['TOOL-CRIMP-01'] && toolMap['TOOL-MULT-01']) {
|
||||||
|
await conn.query('INSERT INTO herramientas_set_items (set_id, herramienta_id, cantidad) VALUES (?, ?, 1), (?, ?, 1), (?, ?, 1)',
|
||||||
|
[set3.insertId, toolMap['TOOL-TEST-01'], set3.insertId, toolMap['TOOL-CRIMP-01'], set3.insertId, toolMap['TOOL-MULT-01']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Seed departments for all ecosystem companies
|
// Seed departments for all ecosystem companies
|
||||||
for (const ec of ecosystemCompanies) {
|
for (const ec of ecosystemCompanies) {
|
||||||
const [depts] = await conn.query('SELECT COUNT(*) as c FROM departments WHERE company_id = ?', [ec.id]);
|
const [depts] = await conn.query('SELECT COUNT(*) as c FROM departments WHERE company_id = ?', [ec.id]);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
const db = require('../config/db');
|
||||||
|
|
||||||
|
class Herramienta {
|
||||||
|
static async findAll(companyId, filtros = {}) {
|
||||||
|
let sql = 'SELECT * FROM herramientas WHERE company_id = ?';
|
||||||
|
const params = [companyId];
|
||||||
|
|
||||||
|
if (filtros.estado) {
|
||||||
|
sql += ' AND estado = ?';
|
||||||
|
params.push(filtros.estado);
|
||||||
|
}
|
||||||
|
if (filtros.categoria) {
|
||||||
|
sql += ' AND categoria = ?';
|
||||||
|
params.push(filtros.categoria);
|
||||||
|
}
|
||||||
|
if (filtros.buscar) {
|
||||||
|
sql += ' AND (nombre LIKE ? OR codigo LIKE ? OR numero_serie LIKE ? OR marca LIKE ?)';
|
||||||
|
const term = `%${filtros.buscar}%`;
|
||||||
|
params.push(term, term, term, term);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY id DESC';
|
||||||
|
const [rows] = await db.query(sql, params);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findById(id, companyId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
'SELECT * FROM herramientas WHERE id = ? AND company_id = ?',
|
||||||
|
[id, companyId]
|
||||||
|
);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(data) {
|
||||||
|
const {
|
||||||
|
company_id,
|
||||||
|
nombre,
|
||||||
|
codigo = null,
|
||||||
|
numero_serie = null,
|
||||||
|
categoria = 'general',
|
||||||
|
marca = null,
|
||||||
|
modelo = null,
|
||||||
|
estado = 'operativo',
|
||||||
|
ubicacion_bodega = null,
|
||||||
|
notas = null
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [result] = await db.query(
|
||||||
|
`INSERT INTO herramientas
|
||||||
|
(company_id, nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[company_id, nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas]
|
||||||
|
);
|
||||||
|
return result.insertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, companyId, data) {
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
const allowed = ['nombre', 'codigo', 'numero_serie', 'categoria', 'marca', 'modelo', 'estado', 'ubicacion_bodega', 'notas'];
|
||||||
|
for (const [key, val] of Object.entries(data)) {
|
||||||
|
if (allowed.includes(key)) {
|
||||||
|
fields.push(`${key} = ?`);
|
||||||
|
values.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) return;
|
||||||
|
values.push(id, companyId);
|
||||||
|
|
||||||
|
await db.query(`UPDATE herramientas SET ${fields.join(', ')} WHERE id = ? AND company_id = ?`, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(id, companyId) {
|
||||||
|
await db.query('DELETE FROM herramientas WHERE id = ? AND company_id = ?', [id, companyId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async countByEstado(companyId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
'SELECT estado, COUNT(*) as count FROM herramientas WHERE company_id = ? GROUP BY estado',
|
||||||
|
[companyId]
|
||||||
|
);
|
||||||
|
const stats = { operativo: 0, en_mantenimiento: 0, danado: 0, baja: 0, total: 0 };
|
||||||
|
rows.forEach(r => {
|
||||||
|
if (stats[r.estado] !== undefined) {
|
||||||
|
stats[r.estado] = r.count;
|
||||||
|
}
|
||||||
|
stats.total += r.count;
|
||||||
|
});
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Herramienta;
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
const db = require('../config/db');
|
||||||
|
|
||||||
|
class HerramientaAsignacion {
|
||||||
|
static async findAll(companyId, filtros = {}) {
|
||||||
|
let sql = `
|
||||||
|
SELECT tha.*,
|
||||||
|
u.nombre as tecnico_nombre,
|
||||||
|
u.email as tecnico_email,
|
||||||
|
hs.nombre as set_nombre,
|
||||||
|
hs.codigo as set_codigo,
|
||||||
|
h.nombre as herramienta_nombre,
|
||||||
|
h.codigo as herramienta_codigo,
|
||||||
|
tv.titulo as visita_titulo,
|
||||||
|
tv.ticket_id,
|
||||||
|
t.asunto as ticket_asunto,
|
||||||
|
adm.nombre as asignado_por_nombre
|
||||||
|
FROM tecnico_herramientas_asignaciones tha
|
||||||
|
INNER JOIN users u ON tha.tecnico_id = u.id
|
||||||
|
LEFT JOIN herramientas_sets hs ON tha.set_id = hs.id
|
||||||
|
LEFT JOIN herramientas h ON tha.herramienta_id = h.id
|
||||||
|
LEFT JOIN ticket_visitas tv ON tha.ticket_visita_id = tv.id
|
||||||
|
LEFT JOIN tickets t ON tv.ticket_id = t.id
|
||||||
|
LEFT JOIN users adm ON tha.asignado_por = adm.id
|
||||||
|
WHERE tha.company_id = ?
|
||||||
|
`;
|
||||||
|
const params = [companyId];
|
||||||
|
|
||||||
|
if (filtros.tecnico_id) {
|
||||||
|
sql += ' AND tha.tecnico_id = ?';
|
||||||
|
params.push(filtros.tecnico_id);
|
||||||
|
}
|
||||||
|
if (filtros.estado) {
|
||||||
|
sql += ' AND tha.estado = ?';
|
||||||
|
params.push(filtros.estado);
|
||||||
|
}
|
||||||
|
if (filtros.tipo_asignacion) {
|
||||||
|
sql += ' AND tha.tipo_asignacion = ?';
|
||||||
|
params.push(filtros.tipo_asignacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY tha.id DESC';
|
||||||
|
const [rows] = await db.query(sql, params);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findByTecnico(companyId, tecnicoId) {
|
||||||
|
return await this.findAll(companyId, { tecnico_id: tecnicoId });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findById(id, companyId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT tha.*,
|
||||||
|
u.nombre as tecnico_nombre,
|
||||||
|
u.email as tecnico_email,
|
||||||
|
hs.nombre as set_nombre,
|
||||||
|
hs.codigo as set_codigo
|
||||||
|
FROM tecnico_herramientas_asignaciones tha
|
||||||
|
INNER JOIN users u ON tha.tecnico_id = u.id
|
||||||
|
LEFT JOIN herramientas_sets hs ON tha.set_id = hs.id
|
||||||
|
WHERE tha.id = ? AND tha.company_id = ?`,
|
||||||
|
[id, companyId]
|
||||||
|
);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(data) {
|
||||||
|
const {
|
||||||
|
company_id,
|
||||||
|
tecnico_id,
|
||||||
|
set_id = null,
|
||||||
|
herramienta_id = null,
|
||||||
|
ticket_visita_id = null,
|
||||||
|
tipo_asignacion = 'dotacion_fija',
|
||||||
|
fecha_asignacion = new Date(),
|
||||||
|
observaciones_entrega = null,
|
||||||
|
asignado_por = null
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [res] = await db.query(
|
||||||
|
`INSERT INTO tecnico_herramientas_asignaciones
|
||||||
|
(company_id, tecnico_id, set_id, herramienta_id, ticket_visita_id, tipo_asignacion, fecha_asignacion, estado, observaciones_entrega, asignado_por)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'asignado', ?, ?)`,
|
||||||
|
[company_id, tecnico_id, set_id, herramienta_id, ticket_visita_id, tipo_asignacion, fecha_asignacion, observaciones_entrega, asignado_por]
|
||||||
|
);
|
||||||
|
return res.insertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async devolver(id, companyId, { observaciones_devolucion = null, estado = 'devuelto' }) {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE tecnico_herramientas_asignaciones
|
||||||
|
SET estado = ?,
|
||||||
|
fecha_devolucion = NOW(),
|
||||||
|
observaciones_devolucion = ?
|
||||||
|
WHERE id = ? AND company_id = ?`,
|
||||||
|
[estado, observaciones_devolucion, id, companyId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(id, companyId) {
|
||||||
|
await db.query('DELETE FROM tecnico_herramientas_asignaciones WHERE id = ? AND company_id = ?', [id, companyId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HerramientaAsignacion;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
const db = require('../config/db');
|
||||||
|
|
||||||
|
class HerramientaSet {
|
||||||
|
static async findAll(companyId) {
|
||||||
|
const [sets] = await db.query(
|
||||||
|
`SELECT hs.*,
|
||||||
|
COUNT(hsi.id) as total_herramientas,
|
||||||
|
COALESCE(SUM(hsi.cantidad), 0) as total_items
|
||||||
|
FROM herramientas_sets hs
|
||||||
|
LEFT JOIN herramientas_set_items hsi ON hs.id = hsi.set_id
|
||||||
|
WHERE hs.company_id = ?
|
||||||
|
GROUP BY hs.id
|
||||||
|
ORDER BY hs.id DESC`,
|
||||||
|
[companyId]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const s of sets) {
|
||||||
|
s.herramientas = await this.getItems(s.id);
|
||||||
|
}
|
||||||
|
return sets;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findById(id, companyId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
'SELECT * FROM herramientas_sets WHERE id = ? AND company_id = ?',
|
||||||
|
[id, companyId]
|
||||||
|
);
|
||||||
|
if (!rows[0]) return null;
|
||||||
|
const set = rows[0];
|
||||||
|
set.herramientas = await this.getItems(set.id);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getItems(setId) {
|
||||||
|
const [items] = await db.query(
|
||||||
|
`SELECT h.*, hsi.cantidad
|
||||||
|
FROM herramientas_set_items hsi
|
||||||
|
INNER JOIN herramientas h ON hsi.herramienta_id = h.id
|
||||||
|
WHERE hsi.set_id = ?
|
||||||
|
ORDER BY h.nombre ASC`,
|
||||||
|
[setId]
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(data, itemIds = []) {
|
||||||
|
const {
|
||||||
|
company_id,
|
||||||
|
nombre,
|
||||||
|
codigo = null,
|
||||||
|
descripcion = null,
|
||||||
|
categoria = 'general',
|
||||||
|
is_active = 1
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [res] = await db.query(
|
||||||
|
`INSERT INTO herramientas_sets (company_id, nombre, codigo, descripcion, categoria, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[company_id, nombre, codigo, descripcion, categoria, is_active]
|
||||||
|
);
|
||||||
|
const setId = res.insertId;
|
||||||
|
|
||||||
|
if (Array.isArray(itemIds) && itemIds.length > 0) {
|
||||||
|
const values = itemIds.map(hId => [setId, parseInt(hId), 1]);
|
||||||
|
await db.query(
|
||||||
|
'INSERT INTO herramientas_set_items (set_id, herramienta_id, cantidad) VALUES ?',
|
||||||
|
[values]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, companyId, data, itemIds = null) {
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
const allowed = ['nombre', 'codigo', 'descripcion', 'categoria', 'is_active'];
|
||||||
|
for (const [key, val] of Object.entries(data)) {
|
||||||
|
if (allowed.includes(key)) {
|
||||||
|
fields.push(`${key} = ?`);
|
||||||
|
values.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length > 0) {
|
||||||
|
values.push(id, companyId);
|
||||||
|
await db.query(`UPDATE herramientas_sets SET ${fields.join(', ')} WHERE id = ? AND company_id = ?`, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(itemIds)) {
|
||||||
|
await db.query('DELETE FROM herramientas_set_items WHERE set_id = ?', [id]);
|
||||||
|
if (itemIds.length > 0) {
|
||||||
|
const valuesItems = itemIds.map(hId => [id, parseInt(hId), 1]);
|
||||||
|
await db.query(
|
||||||
|
'INSERT INTO herramientas_set_items (set_id, herramienta_id, cantidad) VALUES ?',
|
||||||
|
[valuesItems]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async delete(id, companyId) {
|
||||||
|
await db.query('DELETE FROM herramientas_sets WHERE id = ? AND company_id = ?', [id, companyId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HerramientaSet;
|
||||||
@@ -17,13 +17,15 @@ class TicketVisita {
|
|||||||
fecha_fin = null,
|
fecha_fin = null,
|
||||||
estado = 'programada',
|
estado = 'programada',
|
||||||
checklist_json = null,
|
checklist_json = null,
|
||||||
historial_json = null
|
historial_json = null,
|
||||||
|
set_herramientas_id = null,
|
||||||
|
set_herramientas_nombre = null
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO ticket_visitas
|
`INSERT INTO ticket_visitas
|
||||||
(company_id, ticket_id, external_id, tecnico_email, tecnico_nombre, titulo, descripcion, ubicacion, latitud, longitud, fecha_visita, fecha_fin, estado, checklist_json, historial_json)
|
(company_id, ticket_id, external_id, tecnico_email, tecnico_nombre, titulo, descripcion, ubicacion, latitud, longitud, fecha_visita, fecha_fin, estado, checklist_json, historial_json, set_herramientas_id, set_herramientas_nombre)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
company_id,
|
company_id,
|
||||||
ticket_id,
|
ticket_id,
|
||||||
@@ -39,7 +41,9 @@ class TicketVisita {
|
|||||||
fecha_fin,
|
fecha_fin,
|
||||||
estado,
|
estado,
|
||||||
typeof checklist_json === 'object' && checklist_json !== null ? JSON.stringify(checklist_json) : checklist_json,
|
typeof checklist_json === 'object' && checklist_json !== null ? JSON.stringify(checklist_json) : checklist_json,
|
||||||
typeof historial_json === 'object' && historial_json !== null ? JSON.stringify(historial_json) : historial_json
|
typeof historial_json === 'object' && historial_json !== null ? JSON.stringify(historial_json) : historial_json,
|
||||||
|
set_herramientas_id,
|
||||||
|
set_herramientas_nombre
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return result.insertId;
|
return result.insertId;
|
||||||
|
|||||||
+214
@@ -5,6 +5,9 @@ const { isCompanyAdmin } = require('../middleware/auth');
|
|||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Department = require('../models/Department');
|
const Department = require('../models/Department');
|
||||||
const Ticket = require('../models/Ticket');
|
const Ticket = require('../models/Ticket');
|
||||||
|
const Herramienta = require('../models/Herramienta');
|
||||||
|
const HerramientaSet = require('../models/HerramientaSet');
|
||||||
|
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
|
||||||
|
|
||||||
router.get('/dashboard', isCompanyAdmin, async (req, res) => {
|
router.get('/dashboard', isCompanyAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -150,4 +153,215 @@ router.post('/departments/delete/:id', isCompanyAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GESTIÓN DE HERRAMIENTAS Y SETS / KITS FSM
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
router.get('/herramientas', isCompanyAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const companyId = req.session.user.company_id;
|
||||||
|
const [herramientas, sets, asignaciones, stats, allUsers] = await Promise.all([
|
||||||
|
Herramienta.findAll(companyId),
|
||||||
|
HerramientaSet.findAll(companyId),
|
||||||
|
HerramientaAsignacion.findAll(companyId),
|
||||||
|
Herramienta.countByEstado(companyId),
|
||||||
|
User.findAll(companyId)
|
||||||
|
]);
|
||||||
|
const tecnicos = allUsers.filter(u => u.rol === 'agent' || u.rol === 'admin');
|
||||||
|
|
||||||
|
res.render('admin/herramientas', {
|
||||||
|
title: 'Herramientas & Sets',
|
||||||
|
herramientas,
|
||||||
|
sets,
|
||||||
|
asignaciones,
|
||||||
|
stats,
|
||||||
|
tecnicos
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar herramientas:', error);
|
||||||
|
req.flash('error_msg', 'Error al cargar gestión de herramientas');
|
||||||
|
res.redirect('/admin/dashboard');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Crear Herramienta Individual
|
||||||
|
router.post('/herramientas/create', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas } = req.body;
|
||||||
|
if (!nombre) {
|
||||||
|
req.flash('error_msg', 'El nombre de la herramienta es obligatorio');
|
||||||
|
return res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Herramienta.create({
|
||||||
|
company_id: req.session.user.company_id,
|
||||||
|
nombre,
|
||||||
|
codigo: codigo || null,
|
||||||
|
numero_serie: numero_serie || null,
|
||||||
|
categoria: categoria || 'general',
|
||||||
|
marca: marca || null,
|
||||||
|
modelo: modelo || null,
|
||||||
|
estado: estado || 'operativo',
|
||||||
|
ubicacion_bodega: ubicacion_bodega || null,
|
||||||
|
notas: notas || null
|
||||||
|
});
|
||||||
|
req.flash('success_msg', `Herramienta "${nombre}" registrada correctamente`);
|
||||||
|
res.redirect('/admin/herramientas#tab-herramientas');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear herramienta:', error);
|
||||||
|
req.flash('error_msg', 'Error al registrar la herramienta: ' + error.message);
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Editar Herramienta Individual
|
||||||
|
router.post('/herramientas/edit/:id', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { nombre, codigo, numero_serie, categoria, marca, modelo, estado, ubicacion_bodega, notas } = req.body;
|
||||||
|
try {
|
||||||
|
await Herramienta.update(req.params.id, req.session.user.company_id, {
|
||||||
|
nombre,
|
||||||
|
codigo,
|
||||||
|
numero_serie,
|
||||||
|
categoria,
|
||||||
|
marca,
|
||||||
|
modelo,
|
||||||
|
estado,
|
||||||
|
ubicacion_bodega,
|
||||||
|
notas
|
||||||
|
});
|
||||||
|
req.flash('success_msg', 'Herramienta actualizada exitosamente');
|
||||||
|
res.redirect('/admin/herramientas#tab-herramientas');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al actualizar herramienta:', error);
|
||||||
|
req.flash('error_msg', 'Error al actualizar herramienta: ' + error.message);
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eliminar Herramienta Individual
|
||||||
|
router.post('/herramientas/delete/:id', isCompanyAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await Herramienta.delete(req.params.id, req.session.user.company_id);
|
||||||
|
req.flash('success_msg', 'Herramienta eliminada');
|
||||||
|
res.redirect('/admin/herramientas#tab-herramientas');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar herramienta:', error);
|
||||||
|
req.flash('error_msg', 'Error al eliminar herramienta');
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Crear Set / Kit de Herramientas
|
||||||
|
router.post('/herramientas/sets/create', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { nombre, codigo, descripcion, categoria, herramientas_ids } = req.body;
|
||||||
|
if (!nombre) {
|
||||||
|
req.flash('error_msg', 'El nombre del Set / Kit es obligatorio');
|
||||||
|
return res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let items = [];
|
||||||
|
if (Array.isArray(herramientas_ids)) {
|
||||||
|
items = herramientas_ids;
|
||||||
|
} else if (herramientas_ids) {
|
||||||
|
items = [herramientas_ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
await HerramientaSet.create({
|
||||||
|
company_id: req.session.user.company_id,
|
||||||
|
nombre,
|
||||||
|
codigo: codigo || null,
|
||||||
|
descripcion: descripcion || null,
|
||||||
|
categoria: categoria || 'general'
|
||||||
|
}, items);
|
||||||
|
|
||||||
|
req.flash('success_msg', `Kit de herramientas "${nombre}" creado con ${items.length} instrumentos.`);
|
||||||
|
res.redirect('/admin/herramientas#tab-sets');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear set de herramientas:', error);
|
||||||
|
req.flash('error_msg', 'Error al crear set: ' + error.message);
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Editar Set / Kit de Herramientas
|
||||||
|
router.post('/herramientas/sets/edit/:id', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { nombre, codigo, descripcion, categoria, is_active, herramientas_ids } = req.body;
|
||||||
|
try {
|
||||||
|
let items = null;
|
||||||
|
if (herramientas_ids !== undefined) {
|
||||||
|
items = Array.isArray(herramientas_ids) ? herramientas_ids : (herramientas_ids ? [herramientas_ids] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
await HerramientaSet.update(req.params.id, req.session.user.company_id, {
|
||||||
|
nombre,
|
||||||
|
codigo,
|
||||||
|
descripcion,
|
||||||
|
categoria,
|
||||||
|
is_active: is_active === '1' || is_active === 1 ? 1 : 0
|
||||||
|
}, items);
|
||||||
|
|
||||||
|
req.flash('success_msg', 'Set / Kit actualizado exitosamente');
|
||||||
|
res.redirect('/admin/herramientas#tab-sets');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al actualizar set:', error);
|
||||||
|
req.flash('error_msg', 'Error al actualizar set: ' + error.message);
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eliminar Set / Kit de Herramientas
|
||||||
|
router.post('/herramientas/sets/delete/:id', isCompanyAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await HerramientaSet.delete(req.params.id, req.session.user.company_id);
|
||||||
|
req.flash('success_msg', 'Set / Kit eliminado');
|
||||||
|
res.redirect('/admin/herramientas#tab-sets');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar set:', error);
|
||||||
|
req.flash('error_msg', 'Error al eliminar set');
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Asignar Set de Herramientas a Técnico (Dotación Fija)
|
||||||
|
router.post('/herramientas/asignar', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { tecnico_id, set_id, herramienta_id, tipo_asignacion, observaciones_entrega } = req.body;
|
||||||
|
if (!tecnico_id || (!set_id && !herramienta_id)) {
|
||||||
|
req.flash('error_msg', 'Debe seleccionar un técnico y al menos un set o herramienta');
|
||||||
|
return res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await HerramientaAsignacion.create({
|
||||||
|
company_id: req.session.user.company_id,
|
||||||
|
tecnico_id: parseInt(tecnico_id),
|
||||||
|
set_id: set_id ? parseInt(set_id) : null,
|
||||||
|
herramienta_id: herramienta_id ? parseInt(herramienta_id) : null,
|
||||||
|
tipo_asignacion: tipo_asignacion || 'dotacion_fija',
|
||||||
|
observaciones_entrega: observaciones_entrega || 'Asignación administrativa de equipamiento',
|
||||||
|
asignado_por: req.session.user.id
|
||||||
|
});
|
||||||
|
req.flash('success_msg', 'Equipamiento asignado al técnico exitosamente');
|
||||||
|
res.redirect('/admin/herramientas#tab-asignaciones');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al asignar herramientas:', error);
|
||||||
|
req.flash('error_msg', 'Error al asignar equipamiento: ' + error.message);
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registrar Devolución de Herramientas
|
||||||
|
router.post('/herramientas/devolver/:id', isCompanyAdmin, async (req, res) => {
|
||||||
|
const { observaciones_devolucion, estado } = req.body;
|
||||||
|
try {
|
||||||
|
await HerramientaAsignacion.devolver(req.params.id, req.session.user.company_id, {
|
||||||
|
observaciones_devolucion: observaciones_devolucion || 'Devuelto conforme a bodega',
|
||||||
|
estado: estado || 'devuelto'
|
||||||
|
});
|
||||||
|
req.flash('success_msg', 'Devolución de herramientas registrada correctamente');
|
||||||
|
res.redirect('/admin/herramientas#tab-asignaciones');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al registrar devolución:', error);
|
||||||
|
req.flash('error_msg', 'Error al registrar devolución');
|
||||||
|
res.redirect('/admin/herramientas');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
+64
-5
@@ -4,6 +4,9 @@ const { isAgent } = require('../middleware/auth');
|
|||||||
const Ticket = require('../models/Ticket');
|
const Ticket = require('../models/Ticket');
|
||||||
const Department = require('../models/Department');
|
const Department = require('../models/Department');
|
||||||
const TicketVisita = require('../models/TicketVisita');
|
const TicketVisita = require('../models/TicketVisita');
|
||||||
|
const HerramientaSet = require('../models/HerramientaSet');
|
||||||
|
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
|
||||||
|
const User = require('../models/User');
|
||||||
const TareasHubService = require('../config/tareasHubService');
|
const TareasHubService = require('../config/tareasHubService');
|
||||||
|
|
||||||
router.get('/dashboard', isAgent, async (req, res) => {
|
router.get('/dashboard', isAgent, async (req, res) => {
|
||||||
@@ -55,8 +58,9 @@ router.get('/ticket/:id', isAgent, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tecnicosHub = await TareasHubService.obtenerTecnicos();
|
const tecnicosHub = await TareasHubService.obtenerTecnicos();
|
||||||
|
const setsHerramientas = await HerramientaSet.findAll(ticket.company_id);
|
||||||
|
|
||||||
res.render('agent/view-ticket', { ticket, respuestas, agents, visita, tecnicosHub });
|
res.render('agent/view-ticket', { ticket, respuestas, agents, visita, tecnicosHub, setsHerramientas });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
req.flash('error_msg', 'Error al cargar ticket');
|
req.flash('error_msg', 'Error al cargar ticket');
|
||||||
@@ -139,7 +143,8 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
|
|||||||
fechaVisita,
|
fechaVisita,
|
||||||
fechaFin,
|
fechaFin,
|
||||||
prioridad,
|
prioridad,
|
||||||
checklist_items
|
checklist_items,
|
||||||
|
setHerramientasId
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!emailTecnico) {
|
if (!emailTecnico) {
|
||||||
@@ -147,6 +152,12 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
|
|||||||
return res.redirect(`/agent/ticket/${req.params.id}`);
|
return res.redirect(`/agent/ticket/${req.params.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buscar información del set de herramientas si se seleccionó uno
|
||||||
|
let selectedSet = null;
|
||||||
|
if (setHerramientasId) {
|
||||||
|
selectedSet = await HerramientaSet.findById(setHerramientasId, ticket.company_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Procesar checklist
|
// Procesar checklist
|
||||||
let checklistArr = [];
|
let checklistArr = [];
|
||||||
if (typeof checklist_items === 'string') {
|
if (typeof checklist_items === 'string') {
|
||||||
@@ -182,7 +193,7 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Guardar en MariaDB ticket_visitas
|
// 2. Guardar en MariaDB ticket_visitas
|
||||||
await TicketVisita.create({
|
const visitaId = await TicketVisita.create({
|
||||||
company_id: ticket.company_id,
|
company_id: ticket.company_id,
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
external_id: hubRes.tareaId,
|
external_id: hubRes.tareaId,
|
||||||
@@ -195,15 +206,39 @@ router.post('/ticket/:id/visita', isAgent, async (req, res) => {
|
|||||||
fecha_fin: fechaFin ? new Date(fechaFin) : null,
|
fecha_fin: fechaFin ? new Date(fechaFin) : null,
|
||||||
estado: 'programada',
|
estado: 'programada',
|
||||||
checklist_json: checklistArr,
|
checklist_json: checklistArr,
|
||||||
historial_json: hubRes.tarea || null
|
historial_json: hubRes.tarea || null,
|
||||||
|
set_herramientas_id: selectedSet ? selectedSet.id : null,
|
||||||
|
set_herramientas_nombre: selectedSet ? selectedSet.nombre : null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2.1 Vincular asignación de set de herramientas a nivel FSM
|
||||||
|
if (selectedSet) {
|
||||||
|
try {
|
||||||
|
let tecUser = await User.findByEmail(emailTecnico);
|
||||||
|
if (!tecUser) {
|
||||||
|
tecUser = req.session.user;
|
||||||
|
}
|
||||||
|
await HerramientaAsignacion.create({
|
||||||
|
company_id: ticket.company_id,
|
||||||
|
tecnico_id: tecUser.id,
|
||||||
|
set_id: selectedSet.id,
|
||||||
|
ticket_visita_id: visitaId,
|
||||||
|
tipo_asignacion: 'visita_fsm',
|
||||||
|
observaciones_entrega: `Despachado con ${selectedSet.nombre} para visita FSM (Ticket #${ticket.id})`,
|
||||||
|
asignado_por: req.session.user.id
|
||||||
|
});
|
||||||
|
} catch (errAsign) {
|
||||||
|
console.warn('Advertencia registrando asignación FSM de herramientas:', errAsign.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Actualizar estado del ticket a 'en_progreso'
|
// 3. Actualizar estado del ticket a 'en_progreso'
|
||||||
await Ticket.update(ticket.id, { status: 'en_progreso' });
|
await Ticket.update(ticket.id, { status: 'en_progreso' });
|
||||||
|
|
||||||
// 4. Registrar respuesta en el hilo del ticket
|
// 4. Registrar respuesta en el hilo del ticket
|
||||||
const fechaStr = fechaVisita ? new Date(fechaVisita).toLocaleString('es-EC') : 'A coordinar';
|
const fechaStr = fechaVisita ? new Date(fechaVisita).toLocaleString('es-EC') : 'A coordinar';
|
||||||
const msgHilo = `📅 **Visita técnica programada**: Se ha asignado al técnico **${tecnicoNombre || emailTecnico}** para el **${fechaStr}**.\n\n📍 **Ubicación:** ${ubicacion || 'En sitio del cliente'}${referencia ? ` (${referencia})` : ''}\n\n📋 **Checklist operativo inicial:**\n${checklistArr.map(c => `- ${c}`).join('\n')}\n\n*ID de tarea en Hub:* \`${hubRes.tareaId}\``;
|
const kitInfoStr = selectedSet ? `\n🧰 **Set de Herramientas asignado:** ${selectedSet.nombre} (${selectedSet.codigo || 'Kit FSM'})` : '';
|
||||||
|
const msgHilo = `📅 **Visita técnica programada**: Se ha asignado al técnico **${tecnicoNombre || emailTecnico}** para el **${fechaStr}**.${kitInfoStr}\n\n📍 **Ubicación:** ${ubicacion || 'En sitio del cliente'}${referencia ? ` (${referencia})` : ''}\n\n📋 **Checklist operativo inicial:**\n${checklistArr.map(c => `- ${c}`).join('\n')}\n\n*ID de tarea en Hub:* \`${hubRes.tareaId}\``;
|
||||||
await Ticket.addResponse(ticket.id, req.session.user.id, msgHilo);
|
await Ticket.addResponse(ticket.id, req.session.user.id, msgHilo);
|
||||||
|
|
||||||
req.flash('success_msg', 'Visita técnica programada y sincronizada exitosamente con tareas.telcotronics.com');
|
req.flash('success_msg', 'Visita técnica programada y sincronizada exitosamente con tareas.telcotronics.com');
|
||||||
@@ -271,4 +306,28 @@ router.post('/ticket/:id/visita/sync', isAgent, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Panel de "Mis Herramientas & Sets Asignados" para el Técnico / Agente
|
||||||
|
router.get('/herramientas', isAgent, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const companyId = req.session.user.company_id;
|
||||||
|
const tecnicoId = req.session.user.id;
|
||||||
|
|
||||||
|
const asignaciones = await HerramientaAsignacion.findByTecnico(companyId, tecnicoId);
|
||||||
|
|
||||||
|
// Agrupar asignaciones activas e historial
|
||||||
|
const activas = asignaciones.filter(a => a.estado === 'asignado');
|
||||||
|
const historial = asignaciones.filter(a => a.estado !== 'asignado');
|
||||||
|
|
||||||
|
res.render('agent/herramientas', {
|
||||||
|
title: 'Mis Herramientas',
|
||||||
|
activas,
|
||||||
|
historial
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar herramientas del técnico:', error);
|
||||||
|
req.flash('error_msg', 'Error al consultar herramientas asignadas');
|
||||||
|
res.redirect('/agent/dashboard');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const Ticket = require('../models/Ticket');
|
|||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
const Department = require('../models/Department');
|
const Department = require('../models/Department');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
const HerramientaSet = require('../models/HerramientaSet');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
|
* Middleware para validar llamadas M2M mediante API Key o Token de Agente
|
||||||
@@ -351,4 +352,42 @@ async function handleAddNote(req, res) {
|
|||||||
router.post('/external/tickets/:id/notas', handleAddNote);
|
router.post('/external/tickets/:id/notas', handleAddNote);
|
||||||
router.post('/tickets/:id/respuestas', handleAddNote);
|
router.post('/tickets/:id/respuestas', handleAddNote);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/external/herramientas/sets
|
||||||
|
* Consulta los sets y kits de herramientas configurados por empresa
|
||||||
|
*/
|
||||||
|
router.get('/external/herramientas/sets', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const companyId = parseInt(req.query.empresa_id || req.query.company_id || 1);
|
||||||
|
const sets = await HerramientaSet.findAll(companyId);
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
success: true,
|
||||||
|
company_id: companyId,
|
||||||
|
sets: sets.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
nombre: s.nombre,
|
||||||
|
codigo: s.codigo,
|
||||||
|
categoria: s.categoria,
|
||||||
|
descripcion: s.descripcion,
|
||||||
|
total_herramientas: s.total_herramientas,
|
||||||
|
herramientas: s.herramientas ? s.herramientas.map(h => ({
|
||||||
|
id: h.id,
|
||||||
|
nombre: h.nombre,
|
||||||
|
codigo: h.codigo,
|
||||||
|
numero_serie: h.numero_serie,
|
||||||
|
marca: h.marca,
|
||||||
|
modelo: h.modelo,
|
||||||
|
estado: h.estado
|
||||||
|
})) : []
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ ok: false, success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.get('/herramientas/sets', async (req, res) => {
|
||||||
|
return res.redirect(307, '/api/external/herramientas/sets');
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,795 @@
|
|||||||
|
<%- include('../partials/header') %>
|
||||||
|
<% title = 'Herramientas & Sets'; %>
|
||||||
|
|
||||||
|
<!-- Alertas Flash -->
|
||||||
|
<% if (success_msg && success_msg.length > 0) { %>
|
||||||
|
<% success_msg.forEach(msg => { %>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-check-circle me-2 fs-5 text-success"></i>
|
||||||
|
<div><%= msg %></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if (error_msg && error_msg.length > 0) { %>
|
||||||
|
<% error_msg.forEach(msg => { %>
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2 fs-5 text-danger"></i>
|
||||||
|
<div><%= msg %></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Cabecera de Página -->
|
||||||
|
<div class="page-header d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<h1 class="page-title h3 mb-0">Herramientas & Sets de Campo (FSM)</h1>
|
||||||
|
<span class="badge bg-warning-subtle text-dark border px-2.5 py-1" style="font-size: 0.75rem;">
|
||||||
|
<i class="fas fa-toolbox text-warning me-1"></i> Control de Equipamiento
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-0">Administra el inventario de instrumental, kits especializados y asignaciones fijas o por visita presencial a técnicos.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm d-inline-flex align-items-center gap-1.5 shadow-sm" data-bs-toggle="modal" data-bs-target="#modalNuevaHerramienta">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span>Nueva Herramienta</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm fw-bold d-inline-flex align-items-center gap-1.5 shadow-sm text-dark" data-bs-toggle="modal" data-bs-target="#modalNuevoSet">
|
||||||
|
<i class="fas fa-layer-group"></i>
|
||||||
|
<span>Nuevo Set / Kit</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm d-inline-flex align-items-center gap-1.5 shadow-sm" data-bs-toggle="modal" data-bs-target="#modalAsignar">
|
||||||
|
<i class="fas fa-user-tag"></i>
|
||||||
|
<span>Asignar a Técnico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarjetas KPI -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-sm-6 col-xl-3">
|
||||||
|
<div class="app-card p-3 d-flex align-items-center gap-3">
|
||||||
|
<div class="icon-circle bg-primary-subtle text-primary" style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;">
|
||||||
|
<i class="fas fa-tools"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem; letter-spacing: 0.5px;">Total Instrumentos</div>
|
||||||
|
<div class="fs-4 fw-bold text-dark"><%= stats.total %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-xl-3">
|
||||||
|
<div class="app-card p-3 d-flex align-items-center gap-3">
|
||||||
|
<div class="icon-circle bg-success-subtle text-success" style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;">
|
||||||
|
<i class="fas fa-check-double"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem; letter-spacing: 0.5px;">Operativas</div>
|
||||||
|
<div class="fs-4 fw-bold text-success"><%= stats.operativo %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-xl-3">
|
||||||
|
<div class="app-card p-3 d-flex align-items-center gap-3">
|
||||||
|
<div class="icon-circle bg-warning-subtle text-warning" style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;">
|
||||||
|
<i class="fas fa-wrench"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem; letter-spacing: 0.5px;">Mantenimiento / Baja</div>
|
||||||
|
<div class="fs-4 fw-bold text-warning"><%= (stats.en_mantenimiento || 0) + (stats.danado || 0) + (stats.baja || 0) %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-xl-3">
|
||||||
|
<div class="app-card p-3 d-flex align-items-center gap-3">
|
||||||
|
<div class="icon-circle bg-info-subtle text-info" style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem;">
|
||||||
|
<i class="fas fa-boxes-stacked"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem; letter-spacing: 0.5px;">Sets / Kits Configurados</div>
|
||||||
|
<div class="fs-4 fw-bold text-info"><%= sets.length %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pestañas de Navegación -->
|
||||||
|
<ul class="nav nav-pills custom-pills mb-3 gap-2" id="herramientasTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active px-3 py-2 fw-semibold d-flex align-items-center gap-2" id="tab-sets-btn" data-bs-toggle="pill" data-bs-target="#tab-sets" type="button" role="tab">
|
||||||
|
<i class="fas fa-layer-group"></i>
|
||||||
|
<span>Sets / Kits de Herramientas (<%= sets.length %>)</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link px-3 py-2 fw-semibold d-flex align-items-center gap-2" id="tab-herramientas-btn" data-bs-toggle="pill" data-bs-target="#tab-herramientas" type="button" role="tab">
|
||||||
|
<i class="fas fa-toolbox"></i>
|
||||||
|
<span>Inventario Detallado (<%= herramientas.length %>)</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link px-3 py-2 fw-semibold d-flex align-items-center gap-2" id="tab-asignaciones-btn" data-bs-toggle="pill" data-bs-target="#tab-asignaciones" type="button" role="tab">
|
||||||
|
<i class="fas fa-user-check"></i>
|
||||||
|
<span>Asignaciones a Técnicos (<%= asignaciones.length %>)</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="herramientasTabsContent">
|
||||||
|
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- TAB 1: SETS / KITS DE HERRAMIENTAS -->
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<div class="tab-pane fade show active" id="tab-sets" role="tabpanel">
|
||||||
|
<div class="row g-3">
|
||||||
|
<% if (sets.length === 0) { %>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="app-card p-5 text-center">
|
||||||
|
<i class="fas fa-toolbox text-muted fs-1 mb-2"></i>
|
||||||
|
<h5 class="fw-bold text-dark">No hay Sets o Kits creados</h5>
|
||||||
|
<p class="text-muted small mb-3">Agrupa herramientas físicas en sets reutilizables para despacharlos rápidamente en terreno.</p>
|
||||||
|
<button type="button" class="btn btn-warning btn-sm px-3 fw-bold" data-bs-toggle="modal" data-bs-target="#modalNuevoSet">
|
||||||
|
<i class="fas fa-plus me-1"></i> Crear primer Kit de Herramientas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% sets.forEach(s => { %>
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<div class="app-card h-100 p-3.5 d-flex flex-column justify-content-between border shadow-sm rounded-3">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="badge bg-dark text-white font-monospace" style="font-size: 0.7rem;">
|
||||||
|
<%= s.codigo || 'KIT-' + s.id %>
|
||||||
|
</span>
|
||||||
|
<span class="badge <%= s.is_active ? 'bg-success' : 'bg-secondary' %>" style="font-size: 0.68rem;">
|
||||||
|
<%= s.is_active ? 'Activo' : 'Inactivo' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold text-dark mb-1"><%= s.nombre %></h5>
|
||||||
|
<p class="text-muted small mb-3" style="font-size: 0.8rem;"><%= s.descripcion || 'Sin descripción detallada.' %></p>
|
||||||
|
|
||||||
|
<div class="border-top pt-2.5 mb-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="small fw-bold text-dark" style="font-size: 0.75rem;">
|
||||||
|
<i class="fas fa-tools text-primary me-1"></i> Herramientas incluidas (<%= s.herramientas ? s.herramientas.length : 0 %>)
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-light text-muted border" style="font-size: 0.65rem;">
|
||||||
|
Cat: <%= s.categoria %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (s.herramientas && s.herramientas.length > 0) { %>
|
||||||
|
<ul class="list-unstyled mb-0 small" style="font-size: 0.78rem;">
|
||||||
|
<% s.herramientas.forEach(h => { %>
|
||||||
|
<li class="d-flex align-items-center justify-content-between py-1 border-bottom border-light">
|
||||||
|
<div class="text-truncate me-2">
|
||||||
|
<i class="fas fa-check text-success me-1.5" style="font-size: 0.7rem;"></i>
|
||||||
|
<strong><%= h.nombre %></strong>
|
||||||
|
<% if (h.marca) { %><span class="text-muted">(<%= h.marca %>)</span><% } %>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-light text-muted border font-monospace" style="font-size: 0.68rem;">
|
||||||
|
<%= h.codigo || h.numero_serie || 'SN-N/D' %>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="small text-muted fst-italic">No tiene herramientas asociadas aún.</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-3 d-flex align-items-center justify-content-between mt-auto">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary d-inline-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#modalAsignarDirecto<%= s.id %>">
|
||||||
|
<i class="fas fa-user-plus"></i>
|
||||||
|
<span>Asignar</span>
|
||||||
|
</button>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<button type="button" class="btn btn-sm btn-light border" data-bs-toggle="modal" data-bs-target="#modalEditarSet<%= s.id %>" title="Editar Kit">
|
||||||
|
<i class="fas fa-edit text-secondary"></i>
|
||||||
|
</button>
|
||||||
|
<form action="/admin/herramientas/sets/delete/<%= s.id %>" method="POST" onsubmit="return confirm('¿Eliminar este kit de herramientas?');" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-light border text-danger" title="Eliminar Kit">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL EDITAR SET -->
|
||||||
|
<div class="modal fade" id="modalEditarSet<%= s.id %>" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/sets/edit/<%= s.id %>" method="POST">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold">Editar Kit: <%= s.nombre %></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small fw-bold">Nombre del Set / Kit *</label>
|
||||||
|
<input type="text" name="nombre" class="form-control" value="<%= s.nombre %>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Código Identificador</label>
|
||||||
|
<input type="text" name="codigo" class="form-control font-monospace" value="<%= s.codigo || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Categoría Operativa</label>
|
||||||
|
<select name="categoria" class="form-select">
|
||||||
|
<option value="cctv" <%= s.categoria === 'cctv' ? 'selected' : '' %>>Seguridad Electrónica & CCTV</option>
|
||||||
|
<option value="fibra_optica" <%= s.categoria === 'fibra_optica' ? 'selected' : '' %>>Fibra Óptica & Enlaces</option>
|
||||||
|
<option value="redes" <%= s.categoria === 'redes' ? 'selected' : '' %>>Redes & Cableado Estructurado</option>
|
||||||
|
<option value="electricidad" <%= s.categoria === 'electricidad' ? 'selected' : '' %>>Electricidad & Potencia</option>
|
||||||
|
<option value="general" <%= s.categoria === 'general' ? 'selected' : '' %>>Herramientas Generales</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Estado del Kit</label>
|
||||||
|
<select name="is_active" class="form-select">
|
||||||
|
<option value="1" <%= s.is_active ? 'selected' : '' %>>Activo (Disponible para despacho)</option>
|
||||||
|
<option value="0" <%= !s.is_active ? 'selected' : '' %>>Inactivo (Bloqueado temporalmente)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Descripción / Alcance</label>
|
||||||
|
<textarea name="descripcion" class="form-control" rows="2"><%= s.descripcion || '' %></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold mb-1">Seleccionar Herramientas del Inventario</label>
|
||||||
|
<div class="border rounded p-2.5" style="max-height: 200px; overflow-y: auto;">
|
||||||
|
<% const currentItemIds = (s.herramientas || []).map(item => item.id); %>
|
||||||
|
<% herramientas.forEach(h => { %>
|
||||||
|
<div class="form-check py-1">
|
||||||
|
<input class="form-check-input" type="checkbox" name="herramientas_ids" value="<%= h.id %>" id="checkSet<%= s.id %>_<%= h.id %>" <%= currentItemIds.includes(h.id) ? 'checked' : '' %>>
|
||||||
|
<label class="form-check-label small d-flex align-items-center justify-content-between" for="checkSet<%= s.id %>_<%= h.id %>">
|
||||||
|
<span><strong><%= h.nombre %></strong> <% if (h.marca) { %><span class="text-muted">(<%= h.marca %>)</span><% } %></span>
|
||||||
|
<span class="badge bg-light text-muted border font-monospace ms-2"><%= h.codigo || h.numero_serie || 'SN-N/D' %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL ASIGNAR DIRECTO -->
|
||||||
|
<div class="modal fade" id="modalAsignarDirecto<%= s.id %>" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/asignar" method="POST">
|
||||||
|
<input type="hidden" name="set_id" value="<%= s.id %>">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title fw-bold">Asignar Kit: <%= s.nombre %></h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Técnico Responsable *</label>
|
||||||
|
<select name="tecnico_id" class="form-select" required>
|
||||||
|
<option value="">-- Seleccionar Técnico --</option>
|
||||||
|
<% tecnicos.forEach(t => { %>
|
||||||
|
<option value="<%= t.id %>"><%= t.nombre %> (<%= t.email %>)</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Tipo de Asignación</label>
|
||||||
|
<select name="tipo_asignacion" class="form-select">
|
||||||
|
<option value="dotacion_fija">Dotación Fija (Custodia permanente del técnico)</option>
|
||||||
|
<option value="visita_fsm">Asignación Temporal de Turno / Visita</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Observaciones de Entrega</label>
|
||||||
|
<textarea name="observaciones_entrega" class="form-control" rows="2" placeholder="Ej: Entregado completo con estuche y cargadores en buen estado"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary fw-bold">Confirmar Asignación</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- TAB 2: INVENTARIO DETALLADO DE HERRAMIENTAS -->
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<div class="tab-pane fade" id="tab-herramientas" role="tabpanel">
|
||||||
|
<div class="app-card border shadow-sm rounded-3 overflow-hidden">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0" style="font-size: 0.85rem;">
|
||||||
|
<thead class="table-light text-muted small text-uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Código</th>
|
||||||
|
<th>Herramienta / Instrumento</th>
|
||||||
|
<th>Categoría</th>
|
||||||
|
<th>Marca & Modelo</th>
|
||||||
|
<th>Nº Serie</th>
|
||||||
|
<th>Ubicación</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th class="text-end pe-3">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% if (herramientas.length === 0) { %>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4 text-muted">No hay herramientas registradas en el catálogo.</td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% herramientas.forEach(h => { %>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 font-monospace fw-bold text-dark"><%= h.codigo || 'HERR-' + h.id %></td>
|
||||||
|
<td>
|
||||||
|
<strong class="text-dark"><%= h.nombre %></strong>
|
||||||
|
<% if (h.notas) { %><div class="small text-muted text-truncate" style="max-width: 250px;"><%= h.notas %></div><% } %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark border"><%= h.categoria %></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= h.marca || '-' %> <%= h.modelo ? `(${h.modelo})` : '' %>
|
||||||
|
</td>
|
||||||
|
<td class="font-monospace text-muted"><%= h.numero_serie || 'N/D' %></td>
|
||||||
|
<td>
|
||||||
|
<i class="fas fa-warehouse text-muted me-1"></i> <%= h.ubicacion_bodega || 'Bodega Central' %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (h.estado === 'operativo') { %>
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle"><i class="fas fa-check-circle me-1"></i> Operativo</span>
|
||||||
|
<% } else if (h.estado === 'en_mantenimiento') { %>
|
||||||
|
<span class="badge bg-warning-subtle text-warning border border-warning-subtle"><i class="fas fa-wrench me-1"></i> Mantenimiento</span>
|
||||||
|
<% } else if (h.estado === 'danado') { %>
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle"><i class="fas fa-times-circle me-1"></i> Dañado</span>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="badge bg-secondary"><%= h.estado %></span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-3">
|
||||||
|
<button type="button" class="btn btn-sm btn-light border" data-bs-toggle="modal" data-bs-target="#modalEditarHerramienta<%= h.id %>" title="Editar">
|
||||||
|
<i class="fas fa-edit text-secondary"></i>
|
||||||
|
</button>
|
||||||
|
<form action="/admin/herramientas/delete/<%= h.id %>" method="POST" onsubmit="return confirm('¿Eliminar esta herramienta?');" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-light border text-danger" title="Eliminar">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- MODAL EDITAR HERRAMIENTA -->
|
||||||
|
<div class="modal fade" id="modalEditarHerramienta<%= h.id %>" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/edit/<%= h.id %>" method="POST">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold">Editar Herramienta: <%= h.nombre %></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small fw-bold">Nombre *</label>
|
||||||
|
<input type="text" name="nombre" class="form-control" value="<%= h.nombre %>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Código</label>
|
||||||
|
<input type="text" name="codigo" class="form-control font-monospace" value="<%= h.codigo || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Categoría</label>
|
||||||
|
<select name="categoria" class="form-select">
|
||||||
|
<option value="fibra_optica" <%= h.categoria === 'fibra_optica' ? 'selected' : '' %>>Fibra Óptica</option>
|
||||||
|
<option value="cctv" <%= h.categoria === 'cctv' ? 'selected' : '' %>>Seguridad & CCTV</option>
|
||||||
|
<option value="redes" <%= h.categoria === 'redes' ? 'selected' : '' %>>Redes & Cableado</option>
|
||||||
|
<option value="electricidad" <%= h.categoria === 'electricidad' ? 'selected' : '' %>>Electricidad</option>
|
||||||
|
<option value="medicion" <%= h.categoria === 'medicion' ? 'selected' : '' %>>Instrumento de Medición</option>
|
||||||
|
<option value="mano" <%= h.categoria === 'mano' ? 'selected' : '' %>>Herramienta Manual/Poder</option>
|
||||||
|
<option value="general" <%= h.categoria === 'general' ? 'selected' : '' %>>General</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Marca</label>
|
||||||
|
<input type="text" name="marca" class="form-control" value="<%= h.marca || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Modelo</label>
|
||||||
|
<input type="text" name="modelo" class="form-control" value="<%= h.modelo || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Número de Serie</label>
|
||||||
|
<input type="text" name="numero_serie" class="form-control font-monospace" value="<%= h.numero_serie || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Estado Físico / Operativo</label>
|
||||||
|
<select name="estado" class="form-select">
|
||||||
|
<option value="operativo" <%= h.estado === 'operativo' ? 'selected' : '' %>>Operativo (Listo para uso)</option>
|
||||||
|
<option value="en_mantenimiento" <%= h.estado === 'en_mantenimiento' ? 'selected' : '' %>>En Mantenimiento / Calibración</option>
|
||||||
|
<option value="danado" <%= h.estado === 'danado' ? 'selected' : '' %>>Dañado / Requiere Reparación</option>
|
||||||
|
<option value="baja" <%= h.estado === 'baja' ? 'selected' : '' %>>Dado de Baja</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Ubicación en Bodega / Almacén</label>
|
||||||
|
<input type="text" name="ubicacion_bodega" class="form-control" value="<%= h.ubicacion_bodega || '' %>">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Notas Adicionales</label>
|
||||||
|
<textarea name="notas" class="form-control" rows="2"><%= h.notas || '' %></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- TAB 3: ASIGNACIONES A TÉCNICOS -->
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<div class="tab-pane fade" id="tab-asignaciones" role="tabpanel">
|
||||||
|
<div class="app-card border shadow-sm rounded-3 overflow-hidden">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0" style="font-size: 0.85rem;">
|
||||||
|
<thead class="table-light text-muted small text-uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Técnico</th>
|
||||||
|
<th>Kit / Herramienta Asignada</th>
|
||||||
|
<th>Modalidad</th>
|
||||||
|
<th>Fecha Asignación</th>
|
||||||
|
<th>Contexto / Visita</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th class="text-end pe-3">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% if (asignaciones.length === 0) { %>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center py-4 text-muted">No hay asignaciones registradas actualmente.</td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% asignaciones.forEach(a => { %>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3">
|
||||||
|
<div class="fw-bold text-dark"><%= a.tecnico_nombre %></div>
|
||||||
|
<div class="text-muted small" style="font-size: 0.75rem;"><%= a.tecnico_email %></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (a.set_nombre) { %>
|
||||||
|
<div class="fw-bold text-primary"><i class="fas fa-layer-group me-1"></i> <%= a.set_nombre %></div>
|
||||||
|
<% if (a.set_codigo) { %><span class="badge bg-light text-muted border font-monospace" style="font-size: 0.65rem;"><%= a.set_codigo %></span><% } %>
|
||||||
|
<% } else if (a.herramienta_nombre) { %>
|
||||||
|
<div class="fw-bold text-dark"><i class="fas fa-wrench me-1"></i> <%= a.herramienta_nombre %></div>
|
||||||
|
<% if (a.herramienta_codigo) { %><span class="badge bg-light text-muted border font-monospace" style="font-size: 0.65rem;"><%= a.herramienta_codigo %></span><% } %>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="text-muted">Equipamiento general</span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (a.tipo_asignacion === 'dotacion_fija') { %>
|
||||||
|
<span class="badge bg-info-subtle text-info border border-info-subtle"><i class="fas fa-id-badge me-1"></i> Dotación Fija</span>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="badge bg-warning-subtle text-dark border border-warning-subtle"><i class="fas fa-truck-pickup me-1"></i> Visita Terreno</span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><%= new Date(a.fecha_asignacion).toLocaleDateString('es-EC') %></div>
|
||||||
|
<div class="text-muted small" style="font-size: 0.72rem;"><%= new Date(a.fecha_asignacion).toLocaleTimeString('es-EC') %></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (a.ticket_id) { %>
|
||||||
|
<a href="/agent/ticket/<%= a.ticket_id %>" class="text-decoration-none fw-semibold">
|
||||||
|
Ticket #<%= a.ticket_id %>
|
||||||
|
</a>
|
||||||
|
<div class="text-muted small text-truncate" style="max-width: 180px;"><%= a.visita_titulo || a.ticket_asunto || '' %></div>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="text-muted small"><%= a.observaciones_entrega || 'Sin observaciones' %></span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if (a.estado === 'asignado') { %>
|
||||||
|
<span class="badge bg-success"><i class="fas fa-user-check me-1"></i> En Custodia</span>
|
||||||
|
<% } else if (a.estado === 'devuelto') { %>
|
||||||
|
<span class="badge bg-secondary"><i class="fas fa-undo me-1"></i> Devuelto</span>
|
||||||
|
<% if (a.fecha_devolucion) { %>
|
||||||
|
<div class="text-muted" style="font-size: 0.68rem;"><%= new Date(a.fecha_devolucion).toLocaleDateString('es-EC') %></div>
|
||||||
|
<% } %>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="badge bg-warning text-dark"><%= a.estado %></span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-3">
|
||||||
|
<% if (a.estado === 'asignado') { %>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success d-inline-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#modalDevolver<%= a.id %>">
|
||||||
|
<i class="fas fa-undo-alt"></i>
|
||||||
|
<span>Devolver</span>
|
||||||
|
</button>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="text-muted small" style="font-size: 0.75rem;"><%= a.observaciones_devolucion || 'Finalizado' %></span>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- MODAL DEVOLVER -->
|
||||||
|
<div class="modal fade" id="modalDevolver<%= a.id %>" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/devolver/<%= a.id %>" method="POST">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title fw-bold">Registrar Devolución a Bodega</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="small text-muted mb-3">
|
||||||
|
Confirmar el reingreso del kit <strong><%= a.set_nombre || a.herramienta_nombre %></strong> retornado por <strong><%= a.tecnico_nombre %></strong>.
|
||||||
|
</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Condición al Reingreso</label>
|
||||||
|
<select name="estado" class="form-select">
|
||||||
|
<option value="devuelto">Devuelto en Buen Estado / Operativo</option>
|
||||||
|
<option value="observado">Con Observaciones / Ítem Dañado o Faltante</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Observaciones de Devolución</label>
|
||||||
|
<textarea name="observaciones_devolucion" class="form-control" rows="2" placeholder="Ej: Kit revisado e ingresado completo al estante A1 sin novedades"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-success fw-bold">Confirmar Devolución</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- MODALES GLOBALES DE CREACIÓN -->
|
||||||
|
<!-- ============================================== -->
|
||||||
|
|
||||||
|
<!-- MODAL NUEVA HERRAMIENTA -->
|
||||||
|
<div class="modal fade" id="modalNuevaHerramienta" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/create" method="POST">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title fw-bold"><i class="fas fa-plus-circle me-1"></i> Registrar Nueva Herramienta / Instrumento</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small fw-bold">Nombre de la Herramienta *</label>
|
||||||
|
<input type="text" name="nombre" class="form-control" placeholder="Ej: Fusionadora de Fibra Óptica" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Código / Placa Interna</label>
|
||||||
|
<input type="text" name="codigo" class="form-control font-monospace" placeholder="Ej: TOOL-FIB-02">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Categoría</label>
|
||||||
|
<select name="categoria" class="form-select">
|
||||||
|
<option value="cctv">Seguridad Electrónica & CCTV</option>
|
||||||
|
<option value="fibra_optica">Fibra Óptica & Enlaces</option>
|
||||||
|
<option value="redes">Redes & Cableado Estructurado</option>
|
||||||
|
<option value="electricidad">Electricidad & Tableros</option>
|
||||||
|
<option value="medicion">Instrumento de Medición</option>
|
||||||
|
<option value="mano">Herramienta Manual / Eléctrica</option>
|
||||||
|
<option value="general" selected>General</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Marca</label>
|
||||||
|
<input type="text" name="marca" class="form-control" placeholder="Ej: Fluke, DeWalt, Fujikura">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Modelo</label>
|
||||||
|
<input type="text" name="modelo" class="form-control" placeholder="Ej: 70S Pro">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Número de Serie del Fabricante</label>
|
||||||
|
<input type="text" name="numero_serie" class="form-control font-monospace" placeholder="Ej: SN-998241A">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Estado Inicial</label>
|
||||||
|
<select name="estado" class="form-select">
|
||||||
|
<option value="operativo" selected>Operativo (Listo para asignación)</option>
|
||||||
|
<option value="en_mantenimiento">En Mantenimiento</option>
|
||||||
|
<option value="danado">Dañado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Ubicación en Bodega</label>
|
||||||
|
<input type="text" name="ubicacion_bodega" class="form-control" placeholder="Ej: Bodega Central - Gaveta 4, Estante B">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold">Observaciones / Especificaciones</label>
|
||||||
|
<textarea name="notas" class="form-control" rows="2" placeholder="Detalles de calibración, accesorios incluidos o precauciones"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary fw-bold">Guardar en Inventario</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL NUEVO SET / KIT -->
|
||||||
|
<div class="modal fade" id="modalNuevoSet" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/sets/create" method="POST">
|
||||||
|
<div class="modal-header bg-warning text-dark">
|
||||||
|
<h5 class="modal-title fw-bold"><i class="fas fa-layer-group me-1"></i> Crear Set / Kit de Herramientas</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small fw-bold">Nombre del Set / Kit *</label>
|
||||||
|
<input type="text" name="nombre" class="form-control" placeholder="Ej: Kit Seguridad CCTV & Control de Accesos" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold">Código del Kit</label>
|
||||||
|
<input type="text" name="codigo" class="form-control font-monospace" placeholder="Ej: SET-CCTV-02">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Categoría de Especialidad</label>
|
||||||
|
<select name="categoria" class="form-select">
|
||||||
|
<option value="cctv">Seguridad Electrónica & CCTV</option>
|
||||||
|
<option value="fibra_optica">Fibra Óptica & Enlaces</option>
|
||||||
|
<option value="redes">Redes & Cableado Estructurado</option>
|
||||||
|
<option value="electricidad">Electricidad & Potencia</option>
|
||||||
|
<option value="general" selected>General / Multiuso</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">Descripción del Kit</label>
|
||||||
|
<input type="text" name="descripcion" class="form-control" placeholder="Ej: Kit completo para instalación y certificación en campo">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold mb-1">Seleccionar Herramientas a Integrar en este Set</label>
|
||||||
|
<div class="border rounded p-2.5 bg-light" style="max-height: 220px; overflow-y: auto;">
|
||||||
|
<% if (herramientas.length === 0) { %>
|
||||||
|
<div class="small text-muted p-2">No hay herramientas disponibles en el catálogo. Registra herramientas primero.</div>
|
||||||
|
<% } %>
|
||||||
|
<% herramientas.forEach(h => { %>
|
||||||
|
<div class="form-check py-1 border-bottom border-light">
|
||||||
|
<input class="form-check-input" type="checkbox" name="herramientas_ids" value="<%= h.id %>" id="checkNewSet_<%= h.id %>">
|
||||||
|
<label class="form-check-label small d-flex align-items-center justify-content-between" for="checkNewSet_<%= h.id %>">
|
||||||
|
<span>
|
||||||
|
<strong><%= h.nombre %></strong>
|
||||||
|
<% if (h.marca) { %><span class="text-muted">(<%= h.marca %>)</span><% } %>
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-white text-muted border font-monospace ms-2">
|
||||||
|
<%= h.codigo || h.numero_serie || 'SN-N/D' %>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted" style="font-size: 0.72rem;">Podrás marcar los instrumentos que conforman el equipamiento estándar de este kit.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-warning fw-bold text-dark">Crear Kit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL ASIGNAR GENERAL -->
|
||||||
|
<div class="modal fade" id="modalAsignar" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content rounded-3 border-0 shadow">
|
||||||
|
<form action="/admin/herramientas/asignar" method="POST">
|
||||||
|
<div class="modal-header bg-primary text-white">
|
||||||
|
<h5 class="modal-title fw-bold"><i class="fas fa-user-tag me-1"></i> Asignar Set de Herramientas a Técnico</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Técnico Receptor *</label>
|
||||||
|
<select name="tecnico_id" class="form-select" required>
|
||||||
|
<option value="">-- Seleccionar Técnico --</option>
|
||||||
|
<% tecnicos.forEach(t => { %>
|
||||||
|
<option value="<%= t.id %>"><%= t.nombre %> (<%= t.email %>)</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Set / Kit a Asignar *</label>
|
||||||
|
<select name="set_id" class="form-select" required>
|
||||||
|
<option value="">-- Seleccionar Set / Kit --</option>
|
||||||
|
<% sets.forEach(s => { %>
|
||||||
|
<option value="<%= s.id %>"><%= s.nombre %> <%= s.codigo ? `(${s.codigo})` : '' %> — <%= s.total_herramientas || (s.herramientas ? s.herramientas.length : 0) %> herramientas</option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Tipo de Asignación</label>
|
||||||
|
<select name="tipo_asignacion" class="form-select">
|
||||||
|
<option value="dotacion_fija">Dotación Fija (Custodia permanente del técnico)</option>
|
||||||
|
<option value="visita_fsm">Asignación Temporal para Turno / Proyecto</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">Observaciones de Entrega</label>
|
||||||
|
<textarea name="observaciones_entrega" class="form-control" rows="2" placeholder="Ej: Entregado maletín completo con precintos y cargadores"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Cancelar</button>
|
||||||
|
<button type="submit" class="btn btn-primary fw-bold">Confirmar Asignación</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Activar pestaña según el hash de URL (#tab-herramientas, #tab-sets, #tab-asignaciones)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (hash) {
|
||||||
|
const btn = document.querySelector(`button[data-bs-target="${hash}"]`);
|
||||||
|
if (btn) {
|
||||||
|
const tab = new bootstrap.Tab(btn);
|
||||||
|
tab.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<%- include('../partials/header') %>
|
||||||
|
<% title = 'Mis Herramientas'; %>
|
||||||
|
|
||||||
|
<!-- Alertas Flash -->
|
||||||
|
<% if (success_msg && success_msg.length > 0) { %>
|
||||||
|
<% success_msg.forEach(msg => { %>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-check-circle me-2 fs-5 text-success"></i>
|
||||||
|
<div><%= msg %></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if (error_msg && error_msg.length > 0) { %>
|
||||||
|
<% error_msg.forEach(msg => { %>
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2 fs-5 text-danger"></i>
|
||||||
|
<div><%= msg %></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- Cabecera de Página -->
|
||||||
|
<div class="page-header d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<h1 class="page-title h3 mb-0">Mis Herramientas & Kits Asignados</h1>
|
||||||
|
<span class="badge bg-warning-subtle text-dark border px-2.5 py-1" style="font-size: 0.75rem;">
|
||||||
|
<i class="fas fa-toolbox text-warning me-1"></i> Custodia del Técnico
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-0">Revisa tu equipamiento de dotación oficial y los kits asignados para visitas técnicas presenciales.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/agent/dashboard" class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center gap-1.5 shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<span>Volver a Tickets</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kits y Herramientas Activas en Custodia -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="fw-bold text-dark d-flex align-items-center gap-2 mb-3">
|
||||||
|
<i class="fas fa-box-open text-primary"></i>
|
||||||
|
<span>Equipamiento Activo en Custodia (<%= activas.length %>)</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (activas.length === 0) { %>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="app-card p-4 text-center">
|
||||||
|
<i class="fas fa-toolbox text-muted fs-2 mb-2"></i>
|
||||||
|
<h6 class="fw-bold text-dark">No tienes herramientas asignadas en este momento</h6>
|
||||||
|
<p class="text-muted small mb-0">Tu supervisor o administrador te asignará los kits de dotación fija o los kits específicos al programar visitas de terreno.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% activas.forEach(a => { %>
|
||||||
|
<div class="col-md-6 col-xl-4">
|
||||||
|
<div class="app-card h-100 p-3.5 border shadow-sm rounded-3 d-flex flex-column justify-content-between">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="badge <%= a.tipo_asignacion === 'dotacion_fija' ? 'bg-info-subtle text-info border border-info-subtle' : 'bg-warning-subtle text-dark border border-warning-subtle' %>" style="font-size: 0.7rem;">
|
||||||
|
<i class="fas <%= a.tipo_asignacion === 'dotacion_fija' ? 'fa-id-badge' : 'fa-truck-pickup' %> me-1"></i>
|
||||||
|
<%= a.tipo_asignacion === 'dotacion_fija' ? 'Dotación Fija' : 'Visita FSM' %>
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-success" style="font-size: 0.68rem;">
|
||||||
|
<i class="fas fa-check-circle me-1"></i> En Custodia
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="fw-bold text-dark mb-1">
|
||||||
|
<%= a.set_nombre || a.herramienta_nombre || 'Equipamiento Técnico' %>
|
||||||
|
</h5>
|
||||||
|
<% if (a.set_codigo || a.herramienta_codigo) { %>
|
||||||
|
<span class="badge bg-light text-muted border font-monospace mb-2" style="font-size: 0.7rem;">
|
||||||
|
<%= a.set_codigo || a.herramienta_codigo %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div class="small text-muted mb-3" style="font-size: 0.8rem;">
|
||||||
|
<div><i class="fas fa-calendar-alt text-secondary me-1"></i> Asignado el: <strong><%= new Date(a.fecha_asignacion).toLocaleDateString('es-EC') %></strong></div>
|
||||||
|
<% if (a.asignado_por_nombre) { %>
|
||||||
|
<div><i class="fas fa-user-shield text-secondary me-1"></i> Asignado por: <%= a.asignado_por_nombre %></div>
|
||||||
|
<% } %>
|
||||||
|
<% if (a.ticket_id) { %>
|
||||||
|
<div class="mt-1">
|
||||||
|
<i class="fas fa-ticket-alt text-primary me-1"></i>
|
||||||
|
Para Ticket: <a href="/agent/ticket/<%= a.ticket_id %>" class="fw-semibold">#<%= a.ticket_id %></a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (a.observaciones_entrega) { %>
|
||||||
|
<div class="alert alert-light border p-2 small mb-0 text-muted" style="font-size: 0.75rem;">
|
||||||
|
<i class="fas fa-info-circle me-1 text-info"></i> <%= a.observaciones_entrega %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-3 mt-3 d-flex align-items-center justify-content-between">
|
||||||
|
<span class="small text-muted" style="font-size: 0.72rem;">Reportar novedad a bodega</span>
|
||||||
|
<span class="badge bg-light text-dark border font-monospace" style="font-size: 0.7rem;">ID #<%= a.id %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historial de Equipamiento Devuelto -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="fw-bold text-dark d-flex align-items-center gap-2 mb-3">
|
||||||
|
<i class="fas fa-history text-secondary"></i>
|
||||||
|
<span>Historial de Devoluciones (<%= historial.length %>)</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (historial.length > 0) { %>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="app-card border shadow-sm rounded-3 overflow-hidden">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0" style="font-size: 0.83rem;">
|
||||||
|
<thead class="table-light text-muted small text-uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Kit / Herramienta</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Fecha Entrega</th>
|
||||||
|
<th>Fecha Devolución</th>
|
||||||
|
<th>Observaciones</th>
|
||||||
|
<th class="pe-3">Estado</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% historial.forEach(h => { %>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 fw-bold text-dark">
|
||||||
|
<%= h.set_nombre || h.herramienta_nombre %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<%= h.tipo_asignacion === 'dotacion_fija' ? 'Dotación Fija' : 'Visita FSM' %>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><%= new Date(h.fecha_asignacion).toLocaleDateString('es-EC') %></td>
|
||||||
|
<td><%= h.fecha_devolucion ? new Date(h.fecha_devolucion).toLocaleDateString('es-EC') : '-' %></td>
|
||||||
|
<td class="text-muted"><%= h.observaciones_devolucion || h.observaciones_entrega || '-' %></td>
|
||||||
|
<td class="pe-3">
|
||||||
|
<span class="badge <%= h.estado === 'devuelto' ? 'bg-secondary' : 'bg-warning text-dark' %>">
|
||||||
|
<%= h.estado %>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%- include('../partials/footer') %>
|
||||||
@@ -156,6 +156,9 @@
|
|||||||
<% if (visita.ubicacion) { %>
|
<% if (visita.ubicacion) { %>
|
||||||
<div class="mt-1 text-truncate" title="<%= visita.ubicacion %>"><i class="fas fa-map-marker-alt text-danger"></i> <%= visita.ubicacion %></div>
|
<div class="mt-1 text-truncate" title="<%= visita.ubicacion %>"><i class="fas fa-map-marker-alt text-danger"></i> <%= visita.ubicacion %></div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (visita.set_herramientas_nombre) { %>
|
||||||
|
<div class="mt-1 text-primary fw-semibold"><i class="fas fa-toolbox"></i> Kit: <%= visita.set_herramientas_nombre %></div>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -433,6 +436,23 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small fw-bold text-dark">
|
||||||
|
<i class="fas fa-toolbox text-warning me-1"></i> Set de Herramientas Requerido (Kit FSM)
|
||||||
|
</label>
|
||||||
|
<select name="setHerramientasId" class="form-select">
|
||||||
|
<option value="">-- Ninguno / Equipamiento de dotación habitual del técnico --</option>
|
||||||
|
<% if (typeof setsHerramientas !== 'undefined' && setsHerramientas.length > 0) { %>
|
||||||
|
<% setsHerramientas.forEach(s => { %>
|
||||||
|
<option value="<%= s.id %>">
|
||||||
|
<%= s.nombre %> <%= s.codigo ? `(${s.codigo})` : '' %> — <%= s.total_herramientas || (s.herramientas ? s.herramientas.length : 0) %> herramientas incluidas
|
||||||
|
</option>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted" style="font-size: 0.72rem;">El kit seleccionado se vinculará a la orden de trabajo FSM y quedará registrado bajo la custodia del técnico.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small fw-bold text-dark">Fecha y Hora de Arribo <span class="text-danger">*</span></label>
|
<label class="form-label small fw-bold text-dark">Fecha y Hora de Arribo <span class="text-danger">*</span></label>
|
||||||
<input type="datetime-local" name="fechaVisita" class="form-control" required>
|
<input type="datetime-local" name="fechaVisita" class="form-control" required>
|
||||||
|
|||||||
@@ -68,6 +68,14 @@
|
|||||||
<span>Bandeja Tickets</span>
|
<span>Bandeja Tickets</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<% if (user.rol === 'agent') { %>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= typeof title !== 'undefined' && title === 'Mis Herramientas' ? 'active' : '' %>" href="/agent/herramientas">
|
||||||
|
<div class="nav-icon-wrapper"><i class="fas fa-toolbox"></i></div>
|
||||||
|
<span>Mis Herramientas</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (['superadmin', 'admin'].includes(user.rol)) { %>
|
<% if (['superadmin', 'admin'].includes(user.rol)) { %>
|
||||||
@@ -84,6 +92,12 @@
|
|||||||
<span>Departamentos</span>
|
<span>Departamentos</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= typeof title !== 'undefined' && title === 'Herramientas & Sets' ? 'active' : '' %>" href="/admin/herramientas">
|
||||||
|
<div class="nav-icon-wrapper"><i class="fas fa-toolbox"></i></div>
|
||||||
|
<span>Herramientas & Sets</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<li class="sidebar-heading mt-3">SISTEMA</li>
|
<li class="sidebar-heading mt-3">SISTEMA</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user