647 lines
22 KiB
JavaScript
647 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* estados-hs-direct/index.js
|
|
* - Endpoint directo: POST /api/homeserve/change-status
|
|
* - Healthchecks: GET / , GET /health
|
|
* - Test UI: GET /test
|
|
*
|
|
* Credenciales HomeServe:
|
|
* 1) ENV HOMESERVE_USER/HOMESERVE_PASS (+ HOMESERVE_BASE_URL opcional)
|
|
* 2) Firestore doc en HS_CRED_DOC_PATH (ej: providerCredentials/homeserve)
|
|
* 3) Fallbacks: providerCredentials/homeserve -> secrets/homeserve
|
|
*/
|
|
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const { chromium } = require('playwright');
|
|
const admin = require('firebase-admin');
|
|
|
|
const app = express();
|
|
app.use(cors({ origin: true }));
|
|
app.use(express.json({ limit: '1mb' }));
|
|
|
|
// --------------------- Utils ---------------------
|
|
function mustEnv(name) {
|
|
const v = process.env[name];
|
|
if (!v) throw new Error(`Missing env: ${name}`);
|
|
return v;
|
|
}
|
|
|
|
function envBool(name, def = false) {
|
|
const v = (process.env[name] ?? '').toString().trim().toLowerCase();
|
|
if (!v) return def;
|
|
return ['1', 'true', 'yes', 'y', 'on'].includes(v);
|
|
}
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
function nowISO() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function safeStr(x) {
|
|
return (x === undefined || x === null) ? '' : String(x);
|
|
}
|
|
|
|
function normalizeDocPath(p) {
|
|
// esperamos "collection/doc"
|
|
if (!p) return null;
|
|
const t = String(p).trim().replace(/^\/+|\/+$/g, '');
|
|
if (!t) return null;
|
|
const parts = t.split('/').filter(Boolean);
|
|
if (parts.length < 2) return null;
|
|
return { col: parts[0], doc: parts[1] };
|
|
}
|
|
|
|
function pickFirstNonEmpty(...vals) {
|
|
for (const v of vals) {
|
|
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// --------------------- Firebase ---------------------
|
|
function initFirebase() {
|
|
if (!process.env.FIREBASE_PRIVATE_KEY) throw new Error('Missing env: FIREBASE_PRIVATE_KEY');
|
|
if (!admin.apps.length) {
|
|
admin.initializeApp({
|
|
credential: admin.credential.cert({
|
|
projectId: mustEnv('FIREBASE_PROJECT_ID'),
|
|
clientEmail: mustEnv('FIREBASE_CLIENT_EMAIL'),
|
|
privateKey: mustEnv('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
|
}),
|
|
});
|
|
}
|
|
return admin.firestore();
|
|
}
|
|
|
|
// --------------------- Config ---------------------
|
|
const CFG = {
|
|
REQUIRE_AUTH: envBool('REQUIRE_AUTH', false),
|
|
|
|
// Si quieres, puedes dejarlo vacío y se usará providerCredentials/homeserve por defecto
|
|
HS_CRED_DOC_PATH: process.env.HS_CRED_DOC_PATH || process.env.HS_CRED_DOC || process.env.HS_CRED_PATH || '',
|
|
|
|
// Puerto
|
|
PORT: Number(process.env.PORT || process.env.CAPROVER_PORT || 3000),
|
|
|
|
// Base “clientes” por defecto (tu enlace es CGI)
|
|
CLIENTES_CGI_BASE: process.env.CLIENTES_CGI_BASE || 'https://www.clientes.homeserve.es/cgi-bin/fccgi.exe',
|
|
|
|
// Timeouts
|
|
NAV_TIMEOUT: Number(process.env.NAV_TIMEOUT || 120000),
|
|
SEL_TIMEOUT: Number(process.env.SEL_TIMEOUT || 60000),
|
|
};
|
|
|
|
function maskCreds(obj) {
|
|
return {
|
|
...obj,
|
|
pass: obj.pass ? '***' : '',
|
|
};
|
|
}
|
|
|
|
// --------------------- Credenciales ---------------------
|
|
async function getHomeServeCreds(db) {
|
|
// 1) ENV
|
|
const envUser = pickFirstNonEmpty(process.env.HOMESERVE_USER, process.env.HS_USER);
|
|
const envPass = pickFirstNonEmpty(process.env.HOMESERVE_PASS, process.env.HS_PASS);
|
|
const envBaseUrl = pickFirstNonEmpty(process.env.HOMESERVE_BASE_URL, process.env.HS_BASE_URL);
|
|
|
|
if (envUser && envPass) {
|
|
return {
|
|
user: envUser,
|
|
pass: envPass,
|
|
baseUrl: envBaseUrl || CFG.CLIENTES_CGI_BASE,
|
|
cgiBase: CFG.CLIENTES_CGI_BASE,
|
|
source: 'env',
|
|
};
|
|
}
|
|
|
|
// 2) Firestore (intenta varios paths)
|
|
const candidates = [];
|
|
const p = normalizeDocPath(CFG.HS_CRED_DOC_PATH);
|
|
if (p) candidates.push(`${p.col}/${p.doc}`);
|
|
|
|
// Fallbacks IMPORTANTES (tu caso)
|
|
candidates.push('providerCredentials/homeserve');
|
|
candidates.push('secrets/homeserve');
|
|
|
|
const tried = new Set();
|
|
for (const path of candidates) {
|
|
if (!path || tried.has(path)) continue;
|
|
tried.add(path);
|
|
|
|
const dp = normalizeDocPath(path);
|
|
if (!dp) continue;
|
|
|
|
const snap = await db.collection(dp.col).doc(dp.doc).get();
|
|
if (!snap.exists) continue;
|
|
|
|
const d = snap.data() || {};
|
|
|
|
const user = pickFirstNonEmpty(d.user, d.username, d.usuario);
|
|
const pass = pickFirstNonEmpty(d.pass, d.password, d.clave);
|
|
const baseUrl = pickFirstNonEmpty(d.baseUrl, d.url, d.loginUrl, envBaseUrl);
|
|
|
|
if (user && pass) {
|
|
return {
|
|
user,
|
|
pass,
|
|
baseUrl: baseUrl || CFG.CLIENTES_CGI_BASE,
|
|
cgiBase: CFG.CLIENTES_CGI_BASE,
|
|
source: `firestore:${path}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
'HomeServe creds missing. Busca en ENV (HOMESERVE_USER/HOMESERVE_PASS) o crea/usa el doc Firestore providerCredentials/homeserve con { user, pass, baseUrl? }.'
|
|
);
|
|
}
|
|
|
|
// --------------------- Playwright helpers ---------------------
|
|
async function withBrowser(fn) {
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
try {
|
|
return await fn(page);
|
|
} finally {
|
|
await browser.close().catch(() => {});
|
|
}
|
|
}
|
|
|
|
function allFrames(page) {
|
|
return page.frames();
|
|
}
|
|
|
|
async function findLocatorInFrames(page, selector) {
|
|
for (const fr of allFrames(page)) {
|
|
const loc = fr.locator(selector);
|
|
try {
|
|
if (await loc.count()) return { frame: fr, locator: loc };
|
|
} catch (_) {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function clickFirstThatExists(page, selectors, opts = {}) {
|
|
for (const sel of selectors) {
|
|
const hit = await findLocatorInFrames(page, sel);
|
|
if (hit) {
|
|
await hit.locator.first().click(opts);
|
|
return sel;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function fillFirstThatExists(page, selectors, value) {
|
|
for (const sel of selectors) {
|
|
const hit = await findLocatorInFrames(page, sel);
|
|
if (hit) {
|
|
await hit.locator.first().fill(value);
|
|
return sel;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function checkInformoClienteIfNeeded(page, enabled) {
|
|
if (!enabled) return false;
|
|
|
|
// Intento 1: label explícito
|
|
const labelTextVariants = [
|
|
'ya ha informado al Cliente',
|
|
'ya ha informado al cliente',
|
|
'informado al Cliente',
|
|
'informado al cliente',
|
|
'Marque esta casilla',
|
|
'marque esta casilla',
|
|
];
|
|
|
|
for (const txt of labelTextVariants) {
|
|
const sel = `label:has-text("${txt}") >> input[type="checkbox"]`;
|
|
const hit = await findLocatorInFrames(page, sel);
|
|
if (hit) {
|
|
const cb = hit.locator.first();
|
|
if (!(await cb.isChecked())) await cb.check();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Intento 2: búsqueda en DOM (checkbox cerca de texto)
|
|
const done = await page.evaluate((variants) => {
|
|
const norm = (s) => (s || '').toLowerCase();
|
|
const labels = Array.from(document.querySelectorAll('label'));
|
|
for (const l of labels) {
|
|
const t = norm(l.textContent);
|
|
if (!variants.some(v => t.includes(norm(v)))) continue;
|
|
const cb = l.querySelector('input[type="checkbox"]') || document.getElementById(l.getAttribute('for') || '');
|
|
if (cb && cb.type === 'checkbox') {
|
|
cb.checked = true;
|
|
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// fallback: checkbox con texto al lado (layout antiguo)
|
|
const cbs = Array.from(document.querySelectorAll('input[type="checkbox"]'));
|
|
for (const cb of cbs) {
|
|
const parentText = norm(cb.parentElement ? cb.parentElement.textContent : '');
|
|
if (variants.some(v => parentText.includes(norm(v)))) {
|
|
cb.checked = true;
|
|
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}, labelTextVariants);
|
|
|
|
return !!done;
|
|
}
|
|
|
|
async function selectStatusByCode(page, code) {
|
|
// preferimos un <select> donde exista una option con value=code o texto que contenga code
|
|
const ok = await page.evaluate((code) => {
|
|
const selects = Array.from(document.querySelectorAll('select'));
|
|
const norm = (s) => (s || '').toString().trim();
|
|
|
|
// 1) option.value === code
|
|
for (const s of selects) {
|
|
const opt = Array.from(s.options || []).find(o => norm(o.value) === norm(code));
|
|
if (opt) {
|
|
s.value = opt.value;
|
|
s.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 2) option.text contiene code
|
|
for (const s of selects) {
|
|
const opt = Array.from(s.options || []).find(o => norm(o.textContent).includes(norm(code)));
|
|
if (opt) {
|
|
s.value = opt.value;
|
|
s.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}, String(code));
|
|
|
|
if (!ok) throw new Error(`No encuentro el desplegable de estado o la opción para el código ${code}.`);
|
|
}
|
|
|
|
async function loginClientesPortal(page, creds) {
|
|
// Abrimos CGI base
|
|
await page.goto(creds.baseUrl || CFG.CLIENTES_CGI_BASE, { waitUntil: 'domcontentloaded', timeout: CFG.NAV_TIMEOUT });
|
|
|
|
// Login “genérico”: primer input text + primer password
|
|
const userSel = [
|
|
'input[name*="user" i]',
|
|
'input[name*="usuario" i]',
|
|
'input[type="text"]',
|
|
];
|
|
const passSel = [
|
|
'input[name*="pass" i]',
|
|
'input[name*="clave" i]',
|
|
'input[type="password"]',
|
|
];
|
|
const submitSel = [
|
|
'button[type="submit"]',
|
|
'input[type="submit"]',
|
|
'input[type="image"]',
|
|
'button:has-text("Entrar")',
|
|
'button:has-text("Acceder")',
|
|
'button:has-text("Login")',
|
|
];
|
|
|
|
await page.waitForTimeout(500);
|
|
const u = await fillFirstThatExists(page, userSel, creds.user);
|
|
const p = await fillFirstThatExists(page, passSel, creds.pass);
|
|
|
|
if (!u || !p) {
|
|
// puede que ya esté logueado o sea otra pantalla
|
|
// no abortamos aquí; dejamos que el siguiente goto confirme si hay sesión
|
|
return;
|
|
}
|
|
|
|
const clicked = await clickFirstThatExists(page, submitSel);
|
|
if (!clicked) {
|
|
// intentamos Enter
|
|
await page.keyboard.press('Enter').catch(() => {});
|
|
}
|
|
|
|
await page.waitForLoadState('networkidle', { timeout: CFG.NAV_TIMEOUT }).catch(() => {});
|
|
}
|
|
|
|
function buildServiceUrl(serviceNumber) {
|
|
// Tu URL exacta:
|
|
// https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=ver_servicioencurso&Servicio=15251178&Pag=1
|
|
const base = CFG.CLIENTES_CGI_BASE;
|
|
const u = new URL(base);
|
|
u.searchParams.set('w3exec', 'ver_servicioencurso');
|
|
u.searchParams.set('Servicio', String(serviceNumber));
|
|
u.searchParams.set('Pag', '1');
|
|
return u.toString();
|
|
}
|
|
|
|
async function clickChangeState(page) {
|
|
// Tu botón: <input type="IMAGE" name="repaso" ... title="Cambiar el Estado del Servicio">
|
|
const selectors = [
|
|
'input[name="repaso"]',
|
|
'input[title*="Cambiar el Estado" i]',
|
|
'input[src*="estado1.gif" i]',
|
|
'input[type="image"][name*="repaso" i]',
|
|
];
|
|
|
|
const clicked = await clickFirstThatExists(page, selectors, { timeout: CFG.SEL_TIMEOUT }).catch(() => null);
|
|
if (!clicked) throw new Error('No encuentro el botón de "Cambiar estado" (repaso).');
|
|
}
|
|
|
|
async function submitChange(page) {
|
|
const selectors = [
|
|
'input[type="submit"][value*="Enviar" i]',
|
|
'input[type="submit"][value*="Guardar" i]',
|
|
'button:has-text("Enviar")',
|
|
'button:has-text("Guardar")',
|
|
'button:has-text("Aceptar")',
|
|
'input[type="image"][title*="Enviar" i]',
|
|
];
|
|
const clicked = await clickFirstThatExists(page, selectors, { timeout: CFG.SEL_TIMEOUT }).catch(() => null);
|
|
if (!clicked) throw new Error('No encuentro el botón para guardar/enviar el cambio de estado.');
|
|
}
|
|
|
|
async function changeStatusViaClientesPortal(db, reqBody) {
|
|
const creds = await getHomeServeCreds(db);
|
|
|
|
const serviceNumber = safeStr(reqBody.serviceNumber).trim();
|
|
const newStatusValue = safeStr(reqBody.newStatusValue).trim();
|
|
const dateString = safeStr(reqBody.dateString).trim(); // DD/MM/AAAA
|
|
const observation = safeStr(reqBody.observation).trim();
|
|
const informoCliente = !!reqBody.informoCliente;
|
|
|
|
if (!serviceNumber) throw new Error('Missing serviceNumber');
|
|
if (!newStatusValue) throw new Error('Missing newStatusValue');
|
|
|
|
const startedAtISO = nowISO();
|
|
|
|
const result = await withBrowser(async (page) => {
|
|
// Login
|
|
await loginClientesPortal(page, creds);
|
|
|
|
// Abrir servicio
|
|
const serviceUrl = buildServiceUrl(serviceNumber);
|
|
await page.goto(serviceUrl, { waitUntil: 'domcontentloaded', timeout: CFG.NAV_TIMEOUT });
|
|
await page.waitForLoadState('networkidle', { timeout: CFG.NAV_TIMEOUT }).catch(() => {});
|
|
await page.waitForTimeout(800);
|
|
|
|
// Click “cambio de estado”
|
|
await clickChangeState(page);
|
|
|
|
// Esperar que cargue formulario
|
|
await page.waitForLoadState('networkidle', { timeout: CFG.NAV_TIMEOUT }).catch(() => {});
|
|
await page.waitForTimeout(800);
|
|
|
|
// Seleccionar estado por CÓDIGO (307, 348, etc.)
|
|
await selectStatusByCode(page, newStatusValue);
|
|
|
|
// Fecha (si existe un input razonable)
|
|
if (dateString) {
|
|
const dateSelectors = [
|
|
'input[name*="fecha" i]',
|
|
'input[id*="fecha" i]',
|
|
'input[placeholder*="dd" i]',
|
|
'input[type="text"][size="10"]',
|
|
'input[type="text"]',
|
|
];
|
|
// llenamos el primero que exista, pero intentando no pisar user/pass: ya estamos dentro
|
|
await fillFirstThatExists(page, dateSelectors, dateString).catch(() => {});
|
|
}
|
|
|
|
// Observación
|
|
if (observation) {
|
|
const obsSelectors = [
|
|
'textarea[name*="obs" i]',
|
|
'textarea[name*="nota" i]',
|
|
'textarea[id*="obs" i]',
|
|
'textarea',
|
|
];
|
|
await fillFirstThatExists(page, obsSelectors, observation).catch(() => {});
|
|
}
|
|
|
|
// Checkbox “ya he informado al Cliente”
|
|
await checkInformoClienteIfNeeded(page, informoCliente).catch(() => false);
|
|
|
|
// Guardar
|
|
await submitChange(page);
|
|
|
|
await page.waitForLoadState('networkidle', { timeout: CFG.NAV_TIMEOUT }).catch(() => {});
|
|
await page.waitForTimeout(1200);
|
|
|
|
// Verificación “blanda”: volvemos al servicio y comprobamos que la página carga
|
|
await page.goto(serviceUrl, { waitUntil: 'domcontentloaded', timeout: CFG.NAV_TIMEOUT });
|
|
await page.waitForLoadState('networkidle', { timeout: CFG.NAV_TIMEOUT }).catch(() => {});
|
|
const html = await page.content().catch(() => '');
|
|
|
|
return {
|
|
ok: true,
|
|
startedAtISO,
|
|
finishedAtISO: nowISO(),
|
|
usedCreds: maskCreds({ ...creds }),
|
|
serviceUrl,
|
|
// dejamos un “hint” por si quieres inspeccionar rápido
|
|
pageContainsStatusCode: html.includes(String(newStatusValue)),
|
|
};
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
// --------------------- Routes ---------------------
|
|
const db = initFirebase();
|
|
|
|
app.get('/', (req, res) => {
|
|
res.status(200).send('ok');
|
|
});
|
|
|
|
app.get('/health', (req, res) => {
|
|
res.status(200).json({
|
|
ok: true,
|
|
service: 'estados-hs',
|
|
port: CFG.PORT,
|
|
requireAuth: CFG.REQUIRE_AUTH,
|
|
hsCredDocPath: CFG.HS_CRED_DOC_PATH || '(auto)',
|
|
ts: nowISO(),
|
|
});
|
|
});
|
|
|
|
// UI simple de pruebas dentro del propio servicio
|
|
app.get('/test', (req, res) => {
|
|
res.type('html').send(TEST_HTML);
|
|
});
|
|
|
|
app.post('/api/homeserve/change-status', async (req, res) => {
|
|
const startedAtISO = nowISO();
|
|
try {
|
|
const out = await changeStatusViaClientesPortal(db, req.body || {});
|
|
res.status(200).json({ ...out, startedAtISO });
|
|
} catch (e) {
|
|
res.status(500).json({
|
|
ok: false,
|
|
startedAtISO,
|
|
finishedAtISO: nowISO(),
|
|
error: {
|
|
message: String(e?.message || e),
|
|
stack: String(e?.stack || ''),
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
// --------------------- Start ---------------------
|
|
app.listen(CFG.PORT, '0.0.0.0', () => {
|
|
console.log(`[estados-hs] listening on :${CFG.PORT}`);
|
|
console.log(`[estados-hs] HS_CRED_DOC_PATH=${CFG.HS_CRED_DOC_PATH || '(auto providerCredentials/homeserve)'}`);
|
|
console.log(`[estados-hs] REQUIRE_AUTH=${CFG.REQUIRE_AUTH ? 1 : 0}`);
|
|
console.log(`[estados-hs] CLIENTES_CGI_BASE=${CFG.CLIENTES_CGI_BASE}`);
|
|
});
|
|
|
|
// --------------------- Embedded test HTML ---------------------
|
|
const TEST_HTML = `<!doctype html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>estados-hs · prueba</title>
|
|
<style>
|
|
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;margin:0;padding:24px;background:#0b1220;color:#e8eefc}
|
|
.card{max-width:820px;margin:0 auto;background:#111a2e;border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:18px}
|
|
label{display:block;margin:12px 0 6px;font-size:13px;opacity:.85}
|
|
input,select,textarea,button{width:100%;padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#0b1220;color:#e8eefc;box-sizing:border-box}
|
|
textarea{min-height:110px}
|
|
button{cursor:pointer;font-weight:800}
|
|
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
.log{white-space:pre-wrap;background:#0b1220;border-radius:12px;border:1px solid rgba(255,255,255,.12);padding:12px;margin-top:12px;font-size:12px}
|
|
.muted{opacity:.7;font-size:12px}
|
|
.actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
|
|
.check{display:flex;gap:10px;align-items:center;margin-top:12px}
|
|
.check input{width:auto}
|
|
.chip{display:inline-block;padding:6px 10px;border-radius:999px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);font-size:12px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h2 style="margin:0 0 8px">estados-hs · prueba</h2>
|
|
<div class="muted">POST <span class="chip">/api/homeserve/change-status</span></div>
|
|
|
|
<label>Base URL (por si pruebas otro dominio)</label>
|
|
<input id="base" value="" placeholder="https://estados-hs.ms.marsalva.org" />
|
|
|
|
<div class="actions">
|
|
<button id="btnRoot">Probar /</button>
|
|
<button id="btnHealth">Probar /health</button>
|
|
</div>
|
|
|
|
<label>Nº de Servicio</label>
|
|
<input id="serviceNumber" placeholder="15251178" />
|
|
|
|
<div class="row">
|
|
<div>
|
|
<label>Código estado</label>
|
|
<select id="statusCode">
|
|
<option value="303">303 · En espera de Cliente por aceptación Presupuesto</option>
|
|
<option value="307">307 · En espera de Profesional por fecha de inicio de trabajos</option>
|
|
<option value="313">313 · En espera de Profesional por secado de cala, pintura o parquet</option>
|
|
<option value="318">318 · En espera de Profesional por confirmación del Siniestro</option>
|
|
<option value="319">319 · En espera de Profesional por material</option>
|
|
<option value="320">320 · En espera de Profesional por espera de otro gremio</option>
|
|
<option value="321">321 · En espera de Profesional por presupuesto/valoración</option>
|
|
<option value="323">323 · En espera de Profesional por mejora del tiempo</option>
|
|
<option value="326">326 · En espera de Cliente por pago de Factura Contado/Franquicia</option>
|
|
<option value="336">336 · En espera de Profesional por avería en observación</option>
|
|
<option value="342">342 · En espera de Profesional pendiente cobro franquicia</option>
|
|
<option value="345">345 · En espera de Profesional en realización pendiente Terminar</option>
|
|
<option value="348" selected>348 · En espera de Cliente por indicaciones</option>
|
|
<option value="352">352 · En espera de Perjudicado por indicaciones</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Fecha (DD/MM/AAAA)</label>
|
|
<input id="dateString" placeholder="05/01/2026" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="check">
|
|
<input type="checkbox" id="informoCliente" />
|
|
<label for="informoCliente" style="margin:0">Marcar “ya he informado al Cliente”</label>
|
|
</div>
|
|
|
|
<label>Observación (opcional)</label>
|
|
<textarea id="observation" placeholder="Ej: el asegurado prefiere que vaya el lunes."></textarea>
|
|
|
|
<div style="height:10px"></div>
|
|
<button id="btnSend">Enviar cambio de estado</button>
|
|
|
|
<div class="log" id="log">Listo.</div>
|
|
<div class="muted" style="margin-top:10px">
|
|
Consejo rápido: si ves "ok" en <span class="chip">/</span> y JSON en <span class="chip">/health</span>, el servicio está vivo. Si el cambio falla, el JSON de error te dirá en qué paso se atranca.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const $ = (id)=>document.getElementById(id);
|
|
const log = (x)=>{ $('log').textContent = (typeof x==='string'?x:JSON.stringify(x,null,2)); };
|
|
const base = ()=> {
|
|
const b = $('base').value.trim();
|
|
if (b) return b.replace(/\\/$/, '');
|
|
return location.origin;
|
|
};
|
|
|
|
async function probe(path){
|
|
const url = base() + path;
|
|
const r = await fetch(url, { method:'GET' });
|
|
const text = await r.text();
|
|
let parsed = null;
|
|
try { parsed = JSON.parse(text); } catch(_){}
|
|
return { http: r.status, url, response: parsed ?? text };
|
|
}
|
|
|
|
$('btnRoot').addEventListener('click', async () => {
|
|
log(await probe('/'));
|
|
});
|
|
|
|
$('btnHealth').addEventListener('click', async () => {
|
|
log(await probe('/health'));
|
|
});
|
|
|
|
$('btnSend').addEventListener('click', async () => {
|
|
try{
|
|
log('Enviando...');
|
|
const url = base() + '/api/homeserve/change-status';
|
|
const body = {
|
|
serviceNumber: $('serviceNumber').value.trim(),
|
|
newStatusValue: $('statusCode').value.trim(),
|
|
dateString: $('dateString').value.trim(),
|
|
observation: $('observation').value.trim(),
|
|
informoCliente: $('informoCliente').checked
|
|
};
|
|
const r = await fetch(url, {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify(body)
|
|
});
|
|
const j = await r.json().catch(()=>({}));
|
|
log({ http: r.status, url, request: body, response: j });
|
|
}catch(e){
|
|
log(String(e));
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>`; |