feat(herramientas): impresion de etiquetas QR y manifiesto HTML responsive para escaneo de cajas de herramientas
This commit is contained in:
@@ -17,6 +17,7 @@ const adminRoutes = require('./routes/admin');
|
||||
const saasRoutes = require('./routes/saas');
|
||||
const companyRoutes = require('./routes/company');
|
||||
const apiRoutes = require('./routes/api');
|
||||
const qrRoutes = require('./routes/qr');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -53,6 +54,7 @@ app.use('/admin', adminRoutes);
|
||||
app.use('/saas', saasRoutes);
|
||||
app.use('/company', companyRoutes);
|
||||
app.use('/api', apiRoutes);
|
||||
app.use('/qr', qrRoutes);
|
||||
|
||||
const { reportarError } = require('./config/errorReporter');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const crypto = require('crypto');
|
||||
require('dotenv').config();
|
||||
|
||||
async function setup() {
|
||||
@@ -201,6 +202,7 @@ async function setup() {
|
||||
company_id INT NOT NULL,
|
||||
nombre VARCHAR(150) NOT NULL,
|
||||
codigo VARCHAR(50) NULL,
|
||||
qr_token VARCHAR(64) NULL,
|
||||
descripcion TEXT NULL,
|
||||
categoria VARCHAR(100) DEFAULT 'general',
|
||||
foto_url VARCHAR(255) NULL,
|
||||
@@ -209,6 +211,7 @@ async function setup() {
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX (company_id),
|
||||
INDEX (codigo),
|
||||
INDEX (qr_token),
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
@@ -236,6 +239,7 @@ async function setup() {
|
||||
herramienta_id INT NULL,
|
||||
ticket_visita_id INT NULL,
|
||||
tipo_asignacion ENUM('dotacion_fija', 'visita_fsm') DEFAULT 'dotacion_fija',
|
||||
qr_token VARCHAR(64) NULL,
|
||||
fecha_asignacion DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
fecha_devolucion DATETIME NULL,
|
||||
estado ENUM('asignado', 'devuelto', 'observado') DEFAULT 'asignado',
|
||||
@@ -249,6 +253,7 @@ async function setup() {
|
||||
INDEX (company_id),
|
||||
INDEX (tecnico_id),
|
||||
INDEX (ticket_visita_id),
|
||||
INDEX (qr_token),
|
||||
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,
|
||||
@@ -264,6 +269,24 @@ async function setup() {
|
||||
await migrateColumn('tecnico_herramientas_asignaciones', 'foto_entrega_url', 'foto_entrega_url VARCHAR(255) NULL AFTER observaciones_entrega');
|
||||
await migrateColumn('tecnico_herramientas_asignaciones', 'foto_devolucion_url', 'foto_devolucion_url VARCHAR(255) NULL AFTER observaciones_devolucion');
|
||||
|
||||
// Migraciones de tokens QR para escaneo e impresión de etiquetas
|
||||
await migrateColumn('herramientas_sets', 'qr_token', 'qr_token VARCHAR(64) NULL AFTER codigo');
|
||||
await migrateColumn('tecnico_herramientas_asignaciones', 'qr_token', 'qr_token VARCHAR(64) NULL AFTER tipo_asignacion');
|
||||
|
||||
// Backfill de qr_token para sets que aún no tengan uno generado
|
||||
const [setsSinToken] = await conn.query('SELECT id FROM herramientas_sets WHERE qr_token IS NULL');
|
||||
for (const s of setsSinToken) {
|
||||
const token = crypto.randomBytes(12).toString('hex');
|
||||
await conn.query('UPDATE herramientas_sets SET qr_token = ? WHERE id = ?', [token, s.id]);
|
||||
}
|
||||
|
||||
// Backfill de qr_token para asignaciones sin token
|
||||
const [asigSinToken] = await conn.query('SELECT id FROM tecnico_herramientas_asignaciones WHERE qr_token IS NULL');
|
||||
for (const a of asigSinToken) {
|
||||
const token = crypto.randomBytes(12).toString('hex');
|
||||
await conn.query('UPDATE tecnico_herramientas_asignaciones SET qr_token = ? WHERE id = ?', [token, a.id]);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const db = require('../config/db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class HerramientaAsignacion {
|
||||
static async findAll(companyId, filtros = {}) {
|
||||
@@ -70,6 +71,51 @@ class HerramientaAsignacion {
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
static async findByToken(token) {
|
||||
if (!token) return null;
|
||||
const [rows] = await db.query(
|
||||
`SELECT tha.*,
|
||||
u.nombre as tecnico_nombre,
|
||||
u.email as tecnico_email,
|
||||
c.name as company_name,
|
||||
hs.nombre as set_nombre,
|
||||
hs.codigo as set_codigo,
|
||||
hs.foto_url as set_foto_url,
|
||||
h.nombre as herramienta_nombre,
|
||||
h.codigo as herramienta_codigo,
|
||||
h.foto_url as herramienta_foto_url,
|
||||
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
|
||||
INNER JOIN companies c ON tha.company_id = c.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.qr_token = ?`,
|
||||
[token]
|
||||
);
|
||||
if (!rows[0]) return null;
|
||||
const asig = rows[0];
|
||||
|
||||
if (asig.set_id) {
|
||||
const HerramientaSet = require('./HerramientaSet');
|
||||
asig.herramientas = await HerramientaSet.getItems(asig.set_id);
|
||||
} else if (asig.herramienta_id) {
|
||||
const Herramienta = require('./Herramienta');
|
||||
const tool = await Herramienta.findById(asig.herramienta_id, asig.company_id);
|
||||
asig.herramientas = tool ? [tool] : [];
|
||||
} else {
|
||||
asig.herramientas = [];
|
||||
}
|
||||
|
||||
return asig;
|
||||
}
|
||||
|
||||
static async create(data) {
|
||||
const {
|
||||
company_id,
|
||||
@@ -81,14 +127,17 @@ class HerramientaAsignacion {
|
||||
fecha_asignacion = new Date(),
|
||||
observaciones_entrega = null,
|
||||
foto_entrega_url = null,
|
||||
qr_token = null,
|
||||
asignado_por = null
|
||||
} = data;
|
||||
|
||||
const finalQrToken = qr_token || crypto.randomBytes(12).toString('hex');
|
||||
|
||||
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, foto_entrega_url, asignado_por)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'asignado', ?, ?, ?)`,
|
||||
[company_id, tecnico_id, set_id, herramienta_id, ticket_visita_id, tipo_asignacion, fecha_asignacion, observaciones_entrega, foto_entrega_url, asignado_por]
|
||||
(company_id, tecnico_id, set_id, herramienta_id, ticket_visita_id, tipo_asignacion, qr_token, fecha_asignacion, estado, observaciones_entrega, foto_entrega_url, asignado_por)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'asignado', ?, ?, ?)`,
|
||||
[company_id, tecnico_id, set_id, herramienta_id, ticket_visita_id, tipo_asignacion, finalQrToken, fecha_asignacion, observaciones_entrega, foto_entrega_url, asignado_por]
|
||||
);
|
||||
return res.insertId;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const db = require('../config/db');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class HerramientaSet {
|
||||
static async findAll(companyId) {
|
||||
@@ -27,10 +28,45 @@ class HerramientaSet {
|
||||
);
|
||||
if (!rows[0]) return null;
|
||||
const set = rows[0];
|
||||
|
||||
if (!set.qr_token) {
|
||||
set.qr_token = crypto.randomBytes(12).toString('hex');
|
||||
await db.query('UPDATE herramientas_sets SET qr_token = ? WHERE id = ?', [set.qr_token, set.id]);
|
||||
}
|
||||
|
||||
set.herramientas = await this.getItems(set.id);
|
||||
return set;
|
||||
}
|
||||
|
||||
static async findByToken(token) {
|
||||
if (!token) return null;
|
||||
const [rows] = await db.query(
|
||||
`SELECT hs.*, c.name as company_name, c.plan as company_plan
|
||||
FROM herramientas_sets hs
|
||||
INNER JOIN companies c ON hs.company_id = c.id
|
||||
WHERE hs.qr_token = ?`,
|
||||
[token]
|
||||
);
|
||||
if (!rows[0]) return null;
|
||||
const set = rows[0];
|
||||
set.herramientas = await this.getItems(set.id);
|
||||
|
||||
// Obtener asignación activa de custodia si existe
|
||||
const [custodia] = await db.query(
|
||||
`SELECT tha.*, u.nombre as tecnico_nombre, u.email as tecnico_email,
|
||||
tv.titulo as visita_titulo, tv.ticket_id, t.asunto as ticket_asunto
|
||||
FROM tecnico_herramientas_asignaciones tha
|
||||
INNER JOIN users u ON tha.tecnico_id = u.id
|
||||
LEFT JOIN ticket_visitas tv ON tha.ticket_visita_id = tv.id
|
||||
LEFT JOIN tickets t ON tv.ticket_id = t.id
|
||||
WHERE tha.set_id = ? AND tha.estado = 'asignado'
|
||||
ORDER BY tha.id DESC LIMIT 1`,
|
||||
[set.id]
|
||||
);
|
||||
set.custodia = custodia[0] || null;
|
||||
return set;
|
||||
}
|
||||
|
||||
static async getItems(setId) {
|
||||
const [items] = await db.query(
|
||||
`SELECT h.*, hsi.cantidad
|
||||
@@ -51,13 +87,16 @@ class HerramientaSet {
|
||||
descripcion = null,
|
||||
categoria = 'general',
|
||||
foto_url = null,
|
||||
qr_token = null,
|
||||
is_active = 1
|
||||
} = data;
|
||||
|
||||
const finalQrToken = qr_token || crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const [res] = await db.query(
|
||||
`INSERT INTO herramientas_sets (company_id, nombre, codigo, descripcion, categoria, foto_url, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[company_id, nombre, codigo, descripcion, categoria, foto_url, is_active]
|
||||
`INSERT INTO herramientas_sets (company_id, nombre, codigo, qr_token, descripcion, categoria, foto_url, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[company_id, nombre, codigo, finalQrToken, descripcion, categoria, foto_url, is_active]
|
||||
);
|
||||
const setId = res.insertId;
|
||||
|
||||
|
||||
Generated
+307
-1
@@ -19,7 +19,8 @@
|
||||
"express-session": "^1.19.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"morgan": "^1.10.1",
|
||||
"mysql2": "^3.22.3"
|
||||
"mysql2": "^3.22.3",
|
||||
"qrcode": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@@ -45,6 +46,30 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
@@ -160,6 +185,44 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-flash": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz",
|
||||
@@ -235,6 +298,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@@ -253,6 +325,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
@@ -306,6 +384,12 @@
|
||||
"node": ">=0.12.18"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -489,6 +573,19 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -525,6 +622,15 @@
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -649,6 +755,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
@@ -704,6 +819,18 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -956,6 +1083,42 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -965,6 +1128,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
@@ -975,6 +1147,15 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -988,6 +1169,23 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
@@ -1036,6 +1234,21 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -1135,6 +1348,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -1245,6 +1464,32 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1321,11 +1566,72 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@
|
||||
"express-session": "^1.19.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"morgan": "^1.10.1",
|
||||
"mysql2": "^3.22.3"
|
||||
"mysql2": "^3.22.3",
|
||||
"qrcode": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const Herramienta = require('../models/Herramienta');
|
||||
const HerramientaSet = require('../models/HerramientaSet');
|
||||
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
|
||||
const { guardarFoto } = require('../config/uploader');
|
||||
const db = require('../config/db');
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
router.get('/dashboard', isCompanyAdmin, async (req, res) => {
|
||||
try {
|
||||
@@ -401,4 +403,46 @@ router.post('/herramientas/devolver/:id', isCompanyAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Imprimir Etiqueta QR de Set / Caja de Herramientas
|
||||
router.get('/herramientas/sets/:id/etiqueta', isCompanyAdmin, async (req, res) => {
|
||||
try {
|
||||
const companyId = req.session.user.company_id;
|
||||
const set = await HerramientaSet.findById(req.params.id, companyId);
|
||||
if (!set) {
|
||||
req.flash('error_msg', 'Kit de herramientas no encontrado');
|
||||
return res.redirect('/admin/herramientas#tab-sets');
|
||||
}
|
||||
|
||||
const proto = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
const host = req.headers['x-forwarded-host'] || req.get('host');
|
||||
const baseUrl = process.env.APP_URL || `${proto}://${host}`;
|
||||
const scanUrl = `${baseUrl}/qr/set/${set.qr_token}`;
|
||||
|
||||
const qrDataUrl = await QRCode.toDataURL(scanUrl, {
|
||||
width: 260,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff'
|
||||
},
|
||||
errorCorrectionLevel: 'H'
|
||||
});
|
||||
|
||||
const [companyRows] = await db.query('SELECT name FROM companies WHERE id = ?', [companyId]);
|
||||
set.company_name = companyRows[0]?.name || 'Telcotronics';
|
||||
|
||||
res.render('admin/etiqueta_qr', {
|
||||
title: `Etiqueta QR: ${set.nombre}`,
|
||||
set,
|
||||
herramientas: set.herramientas || [],
|
||||
scanUrl,
|
||||
qrDataUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al generar etiqueta QR de kit:', error);
|
||||
req.flash('error_msg', 'Error al generar etiqueta QR: ' + error.message);
|
||||
res.redirect('/admin/herramientas#tab-sets');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const HerramientaSet = require('../models/HerramientaSet');
|
||||
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
|
||||
|
||||
// Escaneo público de Set / Caja de herramientas
|
||||
router.get('/set/:token', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
const set = await HerramientaSet.findByToken(token);
|
||||
|
||||
if (!set) {
|
||||
return res.status(404).render('public/qr_error', {
|
||||
title: 'Caja o Kit no encontrado',
|
||||
mensaje: 'El código QR escaneado no corresponde a ninguna caja de herramientas registrada en el sistema.'
|
||||
});
|
||||
}
|
||||
|
||||
res.render('public/qr_set', {
|
||||
title: `Caja de Herramientas: ${set.nombre}`,
|
||||
set,
|
||||
herramientas: set.herramientas || [],
|
||||
custodia: set.custodia
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al consultar QR de Set:', error);
|
||||
res.status(500).render('public/qr_error', {
|
||||
title: 'Error de Consulta',
|
||||
mensaje: 'Ocurrió un error al cargar la información del kit de herramientas.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Escaneo público de Asignación de custodia
|
||||
router.get('/asignacion/:token', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
const asig = await HerramientaAsignacion.findByToken(token);
|
||||
|
||||
if (!asig) {
|
||||
return res.status(404).render('public/qr_error', {
|
||||
title: 'Asignación no encontrada',
|
||||
mensaje: 'El código QR escaneado no corresponde a ninguna asignación de equipamiento activa.'
|
||||
});
|
||||
}
|
||||
|
||||
res.render('public/qr_asignacion', {
|
||||
title: `Custodia: ${asig.set_nombre || asig.herramienta_nombre}`,
|
||||
asig,
|
||||
herramientas: asig.herramientas || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al consultar QR de Asignación:', error);
|
||||
res.status(500).render('public/qr_error', {
|
||||
title: 'Error de Consulta',
|
||||
mensaje: 'Ocurrió un error al cargar la información de la asignación.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,56 @@
|
||||
const HerramientaSet = require('../models/HerramientaSet');
|
||||
const HerramientaAsignacion = require('../models/HerramientaAsignacion');
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
async function testQrFlow() {
|
||||
console.log('=== TEST DE FLUJO DE CÓDIGOS QR Y MANIFIESTO HTML ===');
|
||||
|
||||
const companyId = 1;
|
||||
|
||||
// 1. Obtener Sets existentes
|
||||
console.log('1. Consultando sets de herramientas de Telcotronics...');
|
||||
const sets = await HerramientaSet.findAll(companyId);
|
||||
if (sets.length === 0) {
|
||||
throw new Error('No hay sets en Telcotronics para probar');
|
||||
}
|
||||
|
||||
const testSet = sets[0];
|
||||
console.log(` ✓ Set encontrado: "${testSet.nombre}" (ID: ${testSet.id}, Código: ${testSet.codigo})`);
|
||||
console.log(` ✓ Token QR: ${testSet.qr_token}`);
|
||||
|
||||
if (!testSet.qr_token) {
|
||||
throw new Error('El set no tiene qr_token');
|
||||
}
|
||||
|
||||
// 2. Probar findByToken
|
||||
console.log('2. Buscando set mediante findByToken(qr_token)...');
|
||||
const setByToken = await HerramientaSet.findByToken(testSet.qr_token);
|
||||
if (!setByToken) {
|
||||
throw new Error('findByToken retornó null');
|
||||
}
|
||||
console.log(` ✓ Set recuperado: ${setByToken.nombre} | Empresa: ${setByToken.company_name} | Herramientas: ${setByToken.herramientas.length}`);
|
||||
console.log(` ✓ Estado custodia actual: ${setByToken.custodia ? 'EN CUSTODIA de ' + setByToken.custodia.tecnico_nombre : 'DISPONIBLE EN BODEGA'}`);
|
||||
|
||||
// 3. Generar código QR con la librería qrcode
|
||||
console.log('3. Generando código QR en formato Data URL...');
|
||||
const scanUrl = `https://soporte.sigmac.app/qr/set/${testSet.qr_token}`;
|
||||
const qrDataUrl = await QRCode.toDataURL(scanUrl, {
|
||||
width: 260,
|
||||
margin: 1,
|
||||
color: { dark: '#000000', light: '#ffffff' }
|
||||
});
|
||||
|
||||
if (!qrDataUrl.startsWith('data:image/png;base64,')) {
|
||||
throw new Error('El QR generado no es un Data URL de PNG válido');
|
||||
}
|
||||
console.log(` ✓ Código QR generado correctamente (longitud base64: ${qrDataUrl.length} bytes)`);
|
||||
console.log(` ✓ URL destino del QR: ${scanUrl}`);
|
||||
|
||||
console.log('=== TEST DE QR Y MANIFIESTO FINALIZADO EXITOSAMENTE ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testQrFlow().catch(err => {
|
||||
console.error('ERROR EN TEST:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Etiqueta QR - <%= set.nombre %> | Telcotronics</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f1f5f9;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #0f172a;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
.action-bar {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
/* Contenedor Etiqueta Adhesiva / Sticker */
|
||||
.label-sticker {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
background: #ffffff;
|
||||
border: 2px solid #0f172a;
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
margin: 2rem auto;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
.label-header {
|
||||
border-bottom: 2px solid #0f172a;
|
||||
padding-bottom: 0.65rem;
|
||||
margin-bottom: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
.qr-wrapper {
|
||||
background: #ffffff;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
.qr-image {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
display: block;
|
||||
}
|
||||
.tools-box {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.76rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
background: #ffffff !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
.label-sticker {
|
||||
box-shadow: none !important;
|
||||
border: 2px solid #000000 !important;
|
||||
margin: 0 auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 95mm !important; /* Estándar térmico / sticker */
|
||||
padding: 4mm !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
.qr-image {
|
||||
width: 48mm !important;
|
||||
height: 48mm !important;
|
||||
}
|
||||
.tools-box {
|
||||
background: #ffffff !important;
|
||||
border: 1px solid #000000 !important;
|
||||
font-size: 8pt !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Barra de Acciones (No se imprime) -->
|
||||
<div class="action-bar no-print">
|
||||
<div class="container d-flex flex-wrap align-items-center justify-content-between gap-2" style="max-width: 700px;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a href="/admin/herramientas" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
<span class="fw-bold small text-muted">Impresión de Etiqueta QR</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a href="<%= scanUrl %>" target="_blank" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-mobile-screen me-1"></i> Probar Escaneo
|
||||
</a>
|
||||
<button type="button" class="btn btn-dark btn-sm fw-bold px-3 shadow-sm" onclick="window.print()">
|
||||
<i class="fas fa-print me-1.5"></i> Imprimir Etiqueta
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Etiqueta QR para pegar en la Caja de Herramientas -->
|
||||
<div class="container" style="max-width: 700px;">
|
||||
<div class="label-sticker">
|
||||
<!-- Cabecera -->
|
||||
<div class="label-header">
|
||||
<div class="d-flex align-items-center justify-content-center gap-1.5 mb-1">
|
||||
<i class="fas fa-toolbox text-dark fs-6"></i>
|
||||
<strong class="text-uppercase" style="letter-spacing: 1.5px; font-size: 0.85rem;">
|
||||
<%= set.company_name || 'TELCOTRONICS' %>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="text-muted fw-bold text-uppercase" style="font-size: 0.65rem; letter-spacing: 0.5px;">
|
||||
Control de Equipamiento & Kits FSM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Identificación del Kit -->
|
||||
<div class="text-center mb-2.5">
|
||||
<h2 class="h5 fw-bold text-dark mb-0"><%= set.nombre %></h2>
|
||||
<div class="d-flex align-items-center justify-content-center gap-2 mt-1">
|
||||
<span class="badge bg-dark font-monospace px-2 py-1" style="font-size: 0.75rem;">
|
||||
<%= set.codigo || 'KIT-' + set.id %>
|
||||
</span>
|
||||
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">
|
||||
<%= set.categoria %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Código QR Central -->
|
||||
<div class="text-center mb-2">
|
||||
<div class="qr-wrapper shadow-sm">
|
||||
<img src="<%= qrDataUrl %>" alt="Código QR" class="qr-image">
|
||||
</div>
|
||||
<div class="text-uppercase fw-bold text-dark mt-1" style="font-size: 0.68rem; letter-spacing: 0.8px;">
|
||||
<i class="fas fa-camera me-1"></i> Escanear para lista y custodia
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen de Herramientas Incluidas en la Caja -->
|
||||
<div class="tools-box">
|
||||
<div class="d-flex align-items-center justify-content-between fw-bold text-dark border-bottom pb-1 mb-1" style="font-size: 0.72rem;">
|
||||
<span><i class="fas fa-list-check me-1"></i> Contenido de la Caja:</span>
|
||||
<span><%= herramientas.length %> ítems</span>
|
||||
</div>
|
||||
<ul class="list-unstyled mb-0" style="line-height: 1.35;">
|
||||
<% if (herramientas.length === 0) { %>
|
||||
<li class="text-muted fst-italic">Sin herramientas asignadas</li>
|
||||
<% } %>
|
||||
<% herramientas.forEach((h, i) => { %>
|
||||
<li class="text-truncate">
|
||||
<strong><%= i + 1 %>.</strong> <%= h.nombre %>
|
||||
<% if (h.marca) { %><span class="text-muted">(<%= h.marca %>)</span><% } %>
|
||||
<% if (h.numero_serie) { %><span class="font-monospace text-dark fw-semibold">• SN: <%= h.numero_serie %></span><% } %>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pie de Etiqueta con Enlace -->
|
||||
<div class="text-center mt-2 pt-1">
|
||||
<div class="font-monospace text-muted small" style="font-size: 0.65rem;">
|
||||
<%= scanUrl %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instrucciones (No se imprimen) -->
|
||||
<div class="no-print text-center text-muted small mt-2" style="max-width: 460px; margin: 0 auto;">
|
||||
<i class="fas fa-info-circle me-1 text-primary"></i>
|
||||
<strong>Tip:</strong> Puedes imprimir esta etiqueta en papel adhesivo o impresora térmica y pegarla en la tapa exterior o interior de la caja de herramientas. Al escanear el QR con cualquier teléfono se abrirá el manifiesto con el checklist de verificación.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -154,9 +154,14 @@
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-1.5">
|
||||
<span class="badge bg-dark text-white font-monospace" style="font-size: 0.7rem;">
|
||||
<%= s.codigo || 'KIT-' + s.id %>
|
||||
</span>
|
||||
<a href="/admin/herramientas/sets/<%= s.id %>/etiqueta" target="_blank" class="badge bg-light text-dark border text-decoration-none d-inline-flex align-items-center gap-1" style="font-size: 0.68rem;" title="Ver e Imprimir Etiqueta QR">
|
||||
<i class="fas fa-qrcode text-primary"></i> <span>QR</span>
|
||||
</a>
|
||||
</div>
|
||||
<span class="badge <%= s.is_active ? 'bg-success' : 'bg-secondary' %>" style="font-size: 0.68rem;">
|
||||
<%= s.is_active ? 'Activo' : 'Inactivo' %>
|
||||
</span>
|
||||
@@ -199,11 +204,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-3 d-flex align-items-center justify-content-between mt-auto">
|
||||
<div class="border-top pt-3 d-flex flex-wrap align-items-center justify-content-between gap-2 mt-auto">
|
||||
<div class="d-flex align-items-center gap-1.5">
|
||||
<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>
|
||||
<a href="/admin/herramientas/sets/<%= s.id %>/etiqueta" target="_blank" class="btn btn-sm btn-outline-dark d-inline-flex align-items-center gap-1" title="Imprimir Etiqueta QR para Caja">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
<span>Etiqueta QR</span>
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
@@ -575,6 +586,11 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<% if (a.qr_token) { %>
|
||||
<a href="/qr/asignacion/<%= a.qr_token %>" target="_blank" class="badge bg-dark-subtle text-dark border text-decoration-none d-inline-flex align-items-center gap-1" style="font-size: 0.7rem;" title="Ver Manifiesto Digital QR">
|
||||
<i class="fas fa-qrcode"></i> <span>Manifiesto QR</span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (a.foto_entrega_url) { %>
|
||||
<a href="<%= a.foto_entrega_url %>" target="_blank" class="badge bg-primary-subtle text-primary border border-primary-subtle text-decoration-none d-inline-flex align-items-center gap-1" style="font-size: 0.7rem;">
|
||||
<i class="fas fa-camera"></i> <span>Foto Entrega</span>
|
||||
@@ -585,8 +601,8 @@
|
||||
<i class="fas fa-camera"></i> <span>Foto Devolución</span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (!a.foto_entrega_url && !a.foto_devolucion_url) { %>
|
||||
<span class="text-muted small" style="font-size: 0.72rem;">Sin fotos</span>
|
||||
<% if (!a.foto_entrega_url && !a.foto_devolucion_url && !a.qr_token) { %>
|
||||
<span class="text-muted small" style="font-size: 0.72rem;">Sin datos</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -106,14 +106,20 @@
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 d-flex flex-wrap gap-1.5">
|
||||
<% if (a.qr_token) { %>
|
||||
<a href="/qr/asignacion/<%= a.qr_token %>" target="_blank" class="badge bg-light text-dark border text-decoration-none d-inline-flex align-items-center gap-1.5 py-1 px-2" style="font-size: 0.72rem;">
|
||||
<i class="fas fa-qrcode text-primary"></i>
|
||||
<span>Manifiesto Digital QR</span>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (a.foto_entrega_url) { %>
|
||||
<div class="mb-2">
|
||||
<a href="<%= a.foto_entrega_url %>" target="_blank" class="badge bg-primary-subtle text-primary border border-primary-subtle text-decoration-none d-inline-flex align-items-center gap-1.5 py-1 px-2" style="font-size: 0.72rem;">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>Ver foto de entrega en custodia</span>
|
||||
<span>Foto Entrega</span>
|
||||
</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;">
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Asignación de Equipamiento | Help Desk</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f1f5f9;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #1e293b;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.header-bar {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
color: white;
|
||||
padding: 1.5rem 1rem;
|
||||
border-bottom: 3px solid #10b981;
|
||||
}
|
||||
.card-custom {
|
||||
background: #ffffff;
|
||||
border-radius: 0.85rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header-bar text-center">
|
||||
<div class="container" style="max-width: 600px;">
|
||||
<div class="d-inline-flex align-items-center gap-2 mb-1">
|
||||
<i class="fas fa-id-badge text-success fs-5"></i>
|
||||
<span class="text-uppercase fw-bold small tracking-wide" style="letter-spacing: 1px; color: #94a3b8;"><%= asig.company_name || 'Telcotronics' %></span>
|
||||
</div>
|
||||
<h1 class="h4 fw-bold mb-1 text-white"><%= asig.set_nombre || asig.herramienta_nombre || 'Equipamiento' %></h1>
|
||||
<div class="d-flex align-items-center justify-content-center gap-2">
|
||||
<span class="badge bg-secondary font-monospace"><%= asig.set_codigo || asig.herramienta_codigo || 'ASIG-' + asig.id %></span>
|
||||
<span class="badge bg-success"><%= asig.tipo_asignacion === 'dotacion_fija' ? 'Dotación Fija' : 'Visita FSM' %></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container" style="max-width: 600px;">
|
||||
<div class="card-custom p-3 p-sm-4 mb-3">
|
||||
|
||||
<!-- Estado -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 p-3 rounded bg-light border">
|
||||
<div>
|
||||
<div class="text-muted small">Técnico Responsable</div>
|
||||
<div class="fw-bold fs-6 text-dark"><%= asig.tecnico_nombre %></div>
|
||||
<div class="small text-muted"><%= asig.tecnico_email %></div>
|
||||
</div>
|
||||
<div>
|
||||
<% if (asig.estado === 'asignado') { %>
|
||||
<span class="badge bg-success fs-6"><i class="fas fa-check-circle me-1"></i> En Custodia</span>
|
||||
<% } else if (asig.estado === 'devuelto') { %>
|
||||
<span class="badge bg-secondary fs-6"><i class="fas fa-undo me-1"></i> Devuelto</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-warning text-dark fs-6"><%= asig.estado %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detalles -->
|
||||
<div class="small text-muted mb-3">
|
||||
<div><i class="fas fa-calendar-alt text-secondary me-1.5"></i> Fecha Asignación: <strong><%= new Date(asig.fecha_asignacion).toLocaleString('es-EC') %></strong></div>
|
||||
<% if (asig.asignado_por_nombre) { %>
|
||||
<div><i class="fas fa-user-shield text-secondary me-1.5"></i> Despachado por: <strong><%= asig.asignado_por_nombre %></strong></div>
|
||||
<% } %>
|
||||
<% if (asig.ticket_id) { %>
|
||||
<div class="mt-1"><i class="fas fa-ticket-alt text-primary me-1.5"></i> Ticket: <strong>#<%= asig.ticket_id %></strong> (<%= asig.visita_titulo || asig.ticket_asunto || '' %>)</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (asig.foto_entrega_url) { %>
|
||||
<div class="mb-3">
|
||||
<div class="small fw-bold text-dark mb-1"><i class="fas fa-camera text-primary me-1"></i> Evidencia Fotográfica de Entrega:</div>
|
||||
<div class="rounded overflow-hidden border">
|
||||
<img src="<%= asig.foto_entrega_url %>" alt="Foto Entrega" style="width: 100%; max-height: 240px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (asig.foto_devolucion_url) { %>
|
||||
<div class="mb-3">
|
||||
<div class="small fw-bold text-dark mb-1"><i class="fas fa-camera text-success me-1"></i> Evidencia Fotográfica de Devolución:</div>
|
||||
<div class="rounded overflow-hidden border">
|
||||
<img src="<%= asig.foto_devolucion_url %>" alt="Foto Devolución" style="width: 100%; max-height: 240px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Lista de Herramientas -->
|
||||
<h6 class="fw-bold text-dark mb-2 mt-3"><i class="fas fa-toolbox text-primary me-1"></i> Herramientas Incluidas (<%= herramientas.length %>)</h6>
|
||||
<div class="list-group rounded shadow-none border mb-3">
|
||||
<% herramientas.forEach(h => { %>
|
||||
<div class="list-group-item p-2.5 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div class="fw-bold text-dark" style="font-size: 0.85rem;"><%= h.nombre %></div>
|
||||
<div class="text-muted small" style="font-size: 0.72rem;">
|
||||
<%= h.marca || '' %> <%= h.modelo ? `(${h.modelo})` : '' %>
|
||||
<% if (h.numero_serie) { %> • SN: <%= h.numero_serie %><% } %>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-light text-muted border font-monospace" style="font-size: 0.68rem;"><%= h.codigo || 'HERR-' + h.id %></span>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary btn-sm w-100 shadow-sm" onclick="window.location.reload()">
|
||||
<i class="fas fa-arrows-rotate me-1"></i> Actualizar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="text-center text-muted small" style="font-size: 0.75rem;">
|
||||
Sistema Help Desk & FSM • Telcotronics Ecosystem
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title || 'QR No Encontrado' %> | Telcotronics Help Desk</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8fafc;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.error-card {
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
|
||||
padding: 2.5rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.icon-box {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background-color: #fee2e2;
|
||||
color: #ef4444;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-card">
|
||||
<div class="icon-box">
|
||||
<i class="fas fa-qrcode"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold text-dark mb-2"><%= title %></h4>
|
||||
<p class="text-muted small mb-4"><%= mensaje %></p>
|
||||
<a href="/" class="btn btn-primary btn-sm px-4 py-2 fw-semibold shadow-sm">
|
||||
<i class="fas fa-home me-1.5"></i> Ir a Help Desk
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= set.nombre %> | Manifiesto de Herramientas</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-dark: #1d4ed8;
|
||||
--surface: #ffffff;
|
||||
--bg: #f1f5f9;
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #1e293b;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.header-bar {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
color: white;
|
||||
padding: 1.5rem 1rem;
|
||||
border-bottom: 3px solid #3b82f6;
|
||||
}
|
||||
.set-card {
|
||||
background: var(--surface);
|
||||
border-radius: 0.85rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
.tool-item {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-item:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.tool-item.checked {
|
||||
background-color: #f0fdf4 !important;
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
.tool-item.checked .tool-name {
|
||||
text-decoration: line-through;
|
||||
color: #64748b;
|
||||
}
|
||||
.badge-tag {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 0.35em 0.65em;
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.header-bar {
|
||||
padding: 1.25rem 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Cabecera Superior -->
|
||||
<header class="header-bar text-center">
|
||||
<div class="container" style="max-width: 600px;">
|
||||
<div class="d-inline-flex align-items-center gap-2 mb-1">
|
||||
<i class="fas fa-toolbox text-warning fs-5"></i>
|
||||
<span class="text-uppercase fw-bold small tracking-wide" style="letter-spacing: 1px; color: #94a3b8;"><%= set.company_name || 'Telcotronics' %></span>
|
||||
</div>
|
||||
<h1 class="h4 fw-bold mb-1 text-white"><%= set.nombre %></h1>
|
||||
<div class="d-flex align-items-center justify-content-center gap-2">
|
||||
<span class="badge bg-secondary font-monospace"><%= set.codigo || 'KIT-' + set.id %></span>
|
||||
<span class="badge bg-primary-subtle text-white border border-primary"><%= set.categoria %></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container" style="max-width: 600px;">
|
||||
<div class="set-card p-3 p-sm-4 mb-3">
|
||||
|
||||
<!-- Fotografía de la Caja/Kit si existe -->
|
||||
<% if (set.foto_url) { %>
|
||||
<div class="mb-3 rounded overflow-hidden border shadow-sm position-relative" style="max-height: 220px; background-color: #000;">
|
||||
<img src="<%= set.foto_url %>" alt="<%= set.nombre %>" style="width: 100%; height: 220px; object-fit: cover;">
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Estado de Custodia Actual -->
|
||||
<div class="mb-3 p-3 rounded-3 border <%= custodia ? 'border-success-subtle bg-success-subtle bg-opacity-25' : 'border-info-subtle bg-info-subtle bg-opacity-25' %>">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="fw-bold small text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
|
||||
<i class="fas <%= custodia ? 'fa-user-check text-success' : 'fa-warehouse text-info' %> me-1"></i>
|
||||
Estado Actual
|
||||
</span>
|
||||
<% if (custodia) { %>
|
||||
<span class="badge bg-success"><i class="fas fa-check-circle me-1"></i> Asignado en Custodia</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-primary"><i class="fas fa-boxes-stacked me-1"></i> En Bodega / Disponible</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (custodia) { %>
|
||||
<div class="small">
|
||||
<div class="fw-bold text-dark fs-6"><%= custodia.tecnico_nombre %></div>
|
||||
<div class="text-muted"><i class="fas fa-envelope text-secondary me-1"></i> <%= custodia.tecnico_email %></div>
|
||||
<div class="text-muted mt-1">
|
||||
<i class="fas fa-calendar-alt text-secondary me-1"></i>
|
||||
Asignado: <strong><%= new Date(custodia.fecha_asignacion).toLocaleString('es-EC') %></strong>
|
||||
<span class="badge bg-light text-dark border ms-1 font-monospace" style="font-size: 0.68rem;">
|
||||
<%= custodia.tipo_asignacion === 'dotacion_fija' ? 'Dotación Fija' : 'Visita FSM' %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<% if (custodia.ticket_id) { %>
|
||||
<div class="mt-2 pt-2 border-top border-success-subtle">
|
||||
<span class="badge bg-primary me-1">Ticket #<%= custodia.ticket_id %></span>
|
||||
<span class="text-dark fw-semibold"><%= custodia.visita_titulo || custodia.ticket_asunto || 'Visita Técnica' %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (custodia.foto_entrega_url) { %>
|
||||
<div class="mt-2 pt-2 border-top border-success-subtle">
|
||||
<a href="<%= custodia.foto_entrega_url %>" target="_blank" class="btn btn-outline-success btn-sm py-1 px-2 d-inline-flex align-items-center gap-1.5" style="font-size: 0.75rem;">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>Ver foto de entrega al técnico</span>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="small text-muted">
|
||||
Esta caja de herramientas se encuentra resguardada en bodega central y está lista para ser asignada a un técnico o visita técnica.
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (set.descripcion) { %>
|
||||
<p class="text-muted small mb-3"><%= set.descripcion %></p>
|
||||
<% } %>
|
||||
|
||||
<!-- Lista de Herramientas (Manifest) -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-2.5">
|
||||
<h5 class="fw-bold text-dark h6 mb-0 d-flex align-items-center gap-1.5">
|
||||
<i class="fas fa-clipboard-check text-primary"></i>
|
||||
<span>Inventario de la Caja (<%= herramientas.length %>)</span>
|
||||
</h5>
|
||||
<span class="badge bg-light text-muted border font-monospace" id="contadorChecklist" style="font-size: 0.7rem;">0 / <%= herramientas.length %> verificadas</span>
|
||||
</div>
|
||||
|
||||
<p class="text-muted small mb-3" style="font-size: 0.78rem;">
|
||||
Toca cada herramienta para marcarla en el checklist físico al revisar el contenido de la caja.
|
||||
</p>
|
||||
|
||||
<% if (herramientas.length === 0) { %>
|
||||
<div class="p-3 text-center border rounded bg-light text-muted small">
|
||||
No hay herramientas asociadas a este kit actualmente.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="list-group rounded-3 shadow-none border" id="checklistContainer">
|
||||
<% herramientas.forEach((h, idx) => { %>
|
||||
<div class="list-group-item list-group-item-action p-2.5 d-flex align-items-center justify-content-between tool-item" onclick="toggleToolCheck(this)">
|
||||
<div class="d-flex align-items-center gap-2.5 overflow-hidden me-2">
|
||||
<input class="form-check-input flex-shrink-0 mt-0" type="checkbox" id="tool_<%= h.id %>" onclick="event.stopPropagation(); toggleToolCheck(this.closest('.tool-item'))">
|
||||
|
||||
<% if (h.foto_url) { %>
|
||||
<a href="<%= h.foto_url %>" target="_blank" onclick="event.stopPropagation();" title="Ver foto">
|
||||
<img src="<%= h.foto_url %>" alt="<%= h.nombre %>" class="rounded border flex-shrink-0" style="width: 44px; height: 44px; object-fit: cover;">
|
||||
</a>
|
||||
<% } else { %>
|
||||
<div class="rounded border bg-light flex-shrink-0 d-flex align-items-center justify-content-center text-muted" style="width: 44px; height: 44px; font-size: 1rem;">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<div class="fw-bold text-dark tool-name text-truncate" style="font-size: 0.88rem;"><%= h.nombre %></div>
|
||||
<div class="text-muted small text-truncate" style="font-size: 0.75rem;">
|
||||
<%= h.marca || '' %> <%= h.modelo ? `(${h.modelo})` : '' %>
|
||||
<% if (h.numero_serie) { %>
|
||||
• <span class="font-monospace">SN: <%= h.numero_serie %></span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 text-end">
|
||||
<span class="badge bg-light text-muted border font-monospace" style="font-size: 0.68rem;"><%= h.codigo || 'HERR-' + h.id %></span>
|
||||
<% if (h.estado === 'operativo') { %>
|
||||
<div class="badge bg-success-subtle text-success border border-success-subtle p-1 mt-1 d-block" style="font-size: 0.62rem;">Operativa</div>
|
||||
<% } else { %>
|
||||
<div class="badge bg-warning-subtle text-dark border p-1 mt-1 d-block" style="font-size: 0.62rem;"><%= h.estado %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<!-- Botones de Acción -->
|
||||
<div class="mt-4 pt-3 border-top d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="reiniciarChecklist()">
|
||||
<i class="fas fa-rotate-left me-1"></i> Reiniciar Checklist
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm px-3 shadow-sm" onclick="window.location.reload()">
|
||||
<i class="fas fa-arrows-rotate me-1"></i> Actualizar Estado
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="text-center text-muted small mt-3" style="font-size: 0.75rem;">
|
||||
Sistema Help Desk & FSM • Telcotronics Ecosystem<br>
|
||||
<span class="text-secondary font-monospace">Token Seguro: <%= set.qr_token %></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleToolCheck(rowElement) {
|
||||
const checkbox = rowElement.querySelector('input[type="checkbox"]');
|
||||
if (event.target !== checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
if (checkbox.checked) {
|
||||
rowElement.classList.add('checked');
|
||||
} else {
|
||||
rowElement.classList.remove('checked');
|
||||
}
|
||||
actualizarContador();
|
||||
}
|
||||
|
||||
function actualizarContador() {
|
||||
const total = document.querySelectorAll('.tool-item').length;
|
||||
const marcados = document.querySelectorAll('.tool-item input[type="checkbox"]:checked').length;
|
||||
const badge = document.getElementById('contadorChecklist');
|
||||
if (badge) {
|
||||
badge.textContent = `${marcados} / ${total} verificadas`;
|
||||
if (marcados === total && total > 0) {
|
||||
badge.className = 'badge bg-success font-monospace';
|
||||
} else {
|
||||
badge.className = 'badge bg-light text-muted border font-monospace';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reiniciarChecklist() {
|
||||
document.querySelectorAll('.tool-item').forEach(el => {
|
||||
el.classList.remove('checked');
|
||||
const cb = el.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
actualizarContador();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user