Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Two Wheeler Scorecard Console</title> | |
| <style> | |
| :root { | |
| --bg: #f4f7fb; | |
| --panel: #ffffff; | |
| --line: #d9e2ec; | |
| --text: #172033; | |
| --muted: #637083; | |
| --accent: #116d6e; | |
| --accent-2: #204b8c; | |
| --danger: #b42318; | |
| --warn: #a15c07; | |
| --ok: #16794c; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: Arial, Helvetica, sans-serif; | |
| } | |
| header { | |
| background: #0d1b2a; | |
| color: white; | |
| padding: 18px 24px; | |
| } | |
| header h1 { | |
| margin: 0; | |
| font-size: 22px; | |
| font-weight: 700; | |
| } | |
| header p { | |
| margin: 6px 0 0; | |
| color: #cbd5e1; | |
| font-size: 14px; | |
| } | |
| main { | |
| display: grid; | |
| grid-template-columns: minmax(360px, 1.05fr) minmax(360px, 0.95fr); | |
| gap: 16px; | |
| padding: 16px; | |
| } | |
| section { | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| min-width: 0; | |
| } | |
| .section-head { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 12px; | |
| align-items: center; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--line); | |
| } | |
| h2 { | |
| margin: 0; | |
| font-size: 17px; | |
| } | |
| .body { | |
| padding: 16px; | |
| } | |
| .row { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .api-row { | |
| display: grid; | |
| grid-template-columns: minmax(220px, 1.2fr) minmax(170px, 0.8fr) minmax(150px, 0.7fr); | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| label { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12px; | |
| margin-bottom: 4px; | |
| } | |
| input, | |
| select, | |
| textarea { | |
| width: 100%; | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| padding: 9px 10px; | |
| background: white; | |
| color: var(--text); | |
| font-size: 14px; | |
| } | |
| textarea { | |
| min-height: 320px; | |
| resize: vertical; | |
| font-family: Consolas, monospace; | |
| line-height: 1.35; | |
| } | |
| button { | |
| border: 0; | |
| border-radius: 6px; | |
| background: var(--accent); | |
| color: white; | |
| cursor: pointer; | |
| font-weight: 700; | |
| padding: 10px 13px; | |
| } | |
| button.secondary { | |
| background: var(--accent-2); | |
| } | |
| button.ghost { | |
| background: #e8eef6; | |
| color: var(--text); | |
| } | |
| button.danger { | |
| background: var(--danger); | |
| } | |
| .actions { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin: 12px 0; | |
| } | |
| .decision { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .metric { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 12px; | |
| background: #fbfdff; | |
| } | |
| .metric span { | |
| color: var(--muted); | |
| display: block; | |
| font-size: 12px; | |
| } | |
| .metric strong { | |
| display: block; | |
| font-size: 18px; | |
| margin-top: 5px; | |
| overflow-wrap: anywhere; | |
| } | |
| .STP { | |
| color: var(--ok); | |
| } | |
| .CONDITIONAL_STP { | |
| color: var(--warn); | |
| } | |
| .NON_STP, | |
| .REJECT { | |
| color: var(--danger); | |
| } | |
| pre { | |
| background: #101827; | |
| color: #dbeafe; | |
| border-radius: 8px; | |
| padding: 12px; | |
| overflow: auto; | |
| max-height: 360px; | |
| font-size: 13px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 13px; | |
| } | |
| th, | |
| td { | |
| border-bottom: 1px solid var(--line); | |
| padding: 9px 7px; | |
| text-align: left; | |
| vertical-align: top; | |
| } | |
| th { | |
| color: var(--muted); | |
| font-weight: 700; | |
| background: #f8fafc; | |
| position: sticky; | |
| top: 0; | |
| } | |
| .table-wrap { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| overflow: auto; | |
| max-height: 390px; | |
| } | |
| .status { | |
| min-height: 20px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| .status.ok { | |
| color: var(--ok); | |
| } | |
| .status.error { | |
| color: var(--danger); | |
| } | |
| .hint { | |
| color: var(--muted); | |
| font-size: 12px; | |
| line-height: 1.45; | |
| margin: 4px 0 12px; | |
| } | |
| .request-line { | |
| background: #eef6f6; | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| color: var(--accent); | |
| font-family: Consolas, monospace; | |
| font-size: 13px; | |
| margin: 8px 0 12px; | |
| overflow-wrap: anywhere; | |
| padding: 9px 10px; | |
| } | |
| .edit-banner { | |
| display: none; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| border: 1px solid #f2c94c; | |
| background: #fff8db; | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| margin-bottom: 12px; | |
| color: #6b4e00; | |
| font-size: 13px; | |
| } | |
| .rule-form { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| .wide { | |
| grid-column: span 2; | |
| } | |
| @media (max-width: 980px) { | |
| main, | |
| .api-row, | |
| .row, | |
| .decision, | |
| .rule-form { | |
| grid-template-columns: 1fr; | |
| } | |
| .wide { | |
| grid-column: auto; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Two Wheeler Loan STP Scorecard Console</h1> | |
| <p>Configure rules, simulate decisions, and generate persisted scorecards from one screen.</p> | |
| </header> | |
| <main> | |
| <section> | |
| <div class="section-head"> | |
| <h2>Scorecard Tester</h2> | |
| <button class="ghost" onclick="loadSample()">Load Sample</button> | |
| </div> | |
| <div class="body"> | |
| <div class="row"> | |
| <div> | |
| <label>Tenant</label> | |
| <input id="tenantId" value="default" /> | |
| </div> | |
| <div> | |
| <label>Product</label> | |
| <select id="productType" onchange="syncProductChange()"> | |
| <option value="TWO_WHEELER_LOAN">Two Wheeler Loan</option> | |
| <option value="NEW_CAR_LOAN">New Car Loan</option> | |
| <option value="USED_CAR_LOAN">Used Car Loan</option> | |
| <option value="NEW_COMMERCIAL_VEHICLE_LOAN">New Commercial Vehicle</option> | |
| <option value="USED_COMMERCIAL_VEHICLE_LOAN">Used Commercial Vehicle</option> | |
| <option value="NEW_TRACTOR_LOAN">New Tractor Loan</option> | |
| <option value="USED_TRACTOR_LOAN">Used Tractor Loan</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Lead ID</label> | |
| <input id="leadId" value="TW10001" /> | |
| </div> | |
| <div> | |
| <label>Journey Trigger</label> | |
| <select id="dueDiligenceStatus"> | |
| <option value="APPROVED">Due Diligence Approved</option> | |
| <option value="PENDING">Due Diligence Pending</option> | |
| <option value="REJECTED">Due Diligence Rejected</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="api-row"> | |
| <div> | |
| <label>API Base URL</label> | |
| <input id="apiBaseUrl" /> | |
| </div> | |
| <div> | |
| <label>Endpoint</label> | |
| <select id="mode" onchange="refreshRequestPreview()"> | |
| <option value="simulate-scorecard">POST /simulate-scorecard</option> | |
| <option value="generate-scorecard">POST /generate-scorecard</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Method</label> | |
| <input id="method" value="POST" disabled /> | |
| </div> | |
| </div> | |
| <div class="hint"> | |
| Use simulate for testing only. Use generate after due diligence approval when you want the scorecard result saved against the lead. | |
| </div> | |
| <div class="request-line" id="requestPreview"></div> | |
| <label>Request JSON</label> | |
| <textarea id="payload"></textarea> | |
| <div class="actions"> | |
| <button onclick="runScorecard()">Send API Request</button> | |
| <button class="secondary" onclick="triggerAfterDueDiligence()">Trigger After Due Diligence</button> | |
| <button class="secondary" onclick="getSavedResult()">Get Saved Result</button> | |
| <button class="ghost" onclick="openDocs()">Swagger Docs</button> | |
| <button class="ghost" onclick="copyCurl()">Copy cURL</button> | |
| </div> | |
| <div class="status" id="scoreStatus"></div> | |
| <div class="decision"> | |
| <div class="metric"> | |
| <span>Decision</span> | |
| <strong id="decision">-</strong> | |
| </div> | |
| <div class="metric"> | |
| <span>Final Score</span> | |
| <strong id="finalScore">-</strong> | |
| </div> | |
| <div class="metric"> | |
| <span>Journey Action</span> | |
| <strong id="journeyAction">-</strong> | |
| </div> | |
| <div class="metric"> | |
| <span>Bureau Risk</span> | |
| <strong id="bureauRisk">-</strong> | |
| </div> | |
| </div> | |
| <pre id="scoreOutput">{}</pre> | |
| <label>Request Preview</label> | |
| <pre id="requestOutput">{}</pre> | |
| </div> | |
| </section> | |
| <section> | |
| <div class="section-head"> | |
| <h2>Rule Configuration</h2> | |
| <button class="ghost" onclick="loadRules()">Refresh Rules</button> | |
| </div> | |
| <div class="body"> | |
| <div class="rule-form"> | |
| <input id="editingRuleId" type="hidden" /> | |
| <div> | |
| <label>Rule Code</label> | |
| <input id="ruleCode" placeholder="CIBIL_SUPER_HIGH" /> | |
| </div> | |
| <div class="wide"> | |
| <label>Rule Name</label> | |
| <input id="ruleName" placeholder="CIBIL score above 800" /> | |
| </div> | |
| <div> | |
| <label>Rule Type</label> | |
| <select id="ruleType"> | |
| <option>SCORE</option> | |
| <option>HARD_REJECT</option> | |
| <option>NON_STP</option> | |
| <option>STP_ELIGIBILITY</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Field Name</label> | |
| <input id="fieldName" placeholder="cibilScore" /> | |
| </div> | |
| <div> | |
| <label>Operator</label> | |
| <select id="operator"> | |
| <option>>=</option> | |
| <option>></option> | |
| <option><=</option> | |
| <option><</option> | |
| <option>=</option> | |
| <option>!=</option> | |
| <option>BETWEEN</option> | |
| <option>IN</option> | |
| <option>NOT_IN</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Value</label> | |
| <input id="fieldValue" placeholder="750 or 700,749" /> | |
| </div> | |
| <div> | |
| <label>Score Value</label> | |
| <input id="scoreValue" placeholder="10" /> | |
| </div> | |
| <div> | |
| <label>Score Component</label> | |
| <input id="scoreComponent" placeholder="cibil" /> | |
| </div> | |
| <div> | |
| <label>Decision Action</label> | |
| <select id="decisionAction"> | |
| <option value="">None</option> | |
| <option>SCORE</option> | |
| <option>STP</option> | |
| <option>UW</option> | |
| <option>REJECT</option> | |
| <option>NON_STP</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Reason Code</label> | |
| <input id="reasonCode" placeholder="HIGH_CIBIL" /> | |
| </div> | |
| <div> | |
| <label>Priority</label> | |
| <input id="priority" value="100" /> | |
| </div> | |
| </div> | |
| <div class="edit-banner" id="editBanner"> | |
| <span id="editText">Editing rule</span> | |
| <button class="ghost" onclick="cancelEdit()">Cancel Edit</button> | |
| </div> | |
| <div class="actions"> | |
| <button id="saveRuleButton" onclick="saveRule()">Create Rule</button> | |
| <button class="ghost" onclick="clearRuleForm()">Clear</button> | |
| <button class="secondary" onclick="exportRules()">Export Product Rules</button> | |
| </div> | |
| <div class="status" id="ruleStatus"></div> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>Code</th> | |
| <th>Type</th> | |
| <th>Condition</th> | |
| <th>Score/Action</th> | |
| <th>Status</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="rulesTable"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| const samplePayload = { | |
| tenantId: "default", | |
| productType: "TWO_WHEELER_LOAN", | |
| application: { | |
| leadId: "TW10001", | |
| customerName: "ABC", | |
| mobile: "9999999999", | |
| employmentType: "SALARIED", | |
| monthlyIncome: 45000, | |
| existingEMI: 8000, | |
| foir: 32, | |
| loanAmount: 95000, | |
| assetCost: 125000, | |
| ltv: 76, | |
| tenure: 36, | |
| dealerId: "DL101", | |
| state: "Karnataka", | |
| city: "Bangalore", | |
| negativeArea: false, | |
| fraudFlag: false, | |
| faceMatchScore: 92, | |
| panVerified: true, | |
| aadhaarVerified: true, | |
| bankingSurrogate: true, | |
| coApplicant: false | |
| }, | |
| bureau: { | |
| cibilScore: 753, | |
| creditVintageMonths: 123, | |
| activeAccounts: 2, | |
| closedAccounts: 47, | |
| overdueAmount: 0, | |
| writtenOffAccounts: 0, | |
| settledAccounts: 0, | |
| active90PlusDpd: false, | |
| maxDPD: 0, | |
| dpd30Count: 0, | |
| dpd60Count: 0, | |
| dpd90Count: 0, | |
| creditEnquiries6M: 2, | |
| creditEnquiries12M: 4, | |
| activeUnsecuredLoans: 1, | |
| activeSecuredLoans: 1, | |
| jointAccounts: 0, | |
| creditUtilization: 2 | |
| } | |
| }; | |
| function setStatus(id, message) { | |
| const element = document.getElementById(id); | |
| element.textContent = message; | |
| element.className = "status"; | |
| } | |
| function setStatusState(id, message, state) { | |
| const element = document.getElementById(id); | |
| element.textContent = message; | |
| element.className = `status ${state || ""}`; | |
| } | |
| function loadSample() { | |
| document.getElementById("payload").value = JSON.stringify(samplePayload, null, 2); | |
| document.getElementById("leadId").value = samplePayload.application.leadId; | |
| refreshRequestPreview(); | |
| } | |
| function syncProductChange() { | |
| try { | |
| const payload = JSON.parse(document.getElementById("payload").value); | |
| payload.productType = document.getElementById("productType").value; | |
| document.getElementById("payload").value = JSON.stringify(payload, null, 2); | |
| } catch (_) { | |
| // Keep user-entered JSON untouched if it is mid-edit. | |
| } | |
| cancelEdit(); | |
| loadRules(); | |
| refreshRequestPreview(); | |
| } | |
| function syncHeaderFields(payload) { | |
| payload.tenantId = document.getElementById("tenantId").value || "default"; | |
| payload.productType = document.getElementById("productType").value || "TWO_WHEELER_LOAN"; | |
| payload.application.leadId = document.getElementById("leadId").value || payload.application.leadId; | |
| return payload; | |
| } | |
| function getBaseUrl() { | |
| return (document.getElementById("apiBaseUrl").value || window.location.origin).replace(/\/$/, ""); | |
| } | |
| function buildUrl(path) { | |
| return `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`; | |
| } | |
| function getScorecardPath() { | |
| return `/${document.getElementById("mode").value}`; | |
| } | |
| function refreshRequestPreview() { | |
| const path = getScorecardPath(); | |
| document.getElementById("requestPreview").textContent = `POST ${buildUrl(path)}`; | |
| } | |
| async function api(path, options = {}) { | |
| const response = await fetch(buildUrl(path), { | |
| headers: { "Content-Type": "application/json", ...(options.headers || {}) }, | |
| ...options | |
| }); | |
| const text = await response.text(); | |
| let body = null; | |
| try { | |
| body = text ? JSON.parse(text) : null; | |
| } catch (_) { | |
| body = text; | |
| } | |
| if (!response.ok) { | |
| throw new Error(body?.detail || body || response.statusText); | |
| } | |
| return { body, status: response.status, statusText: response.statusText }; | |
| } | |
| function readPayload() { | |
| const payload = syncHeaderFields(JSON.parse(document.getElementById("payload").value)); | |
| document.getElementById("payload").value = JSON.stringify(payload, null, 2); | |
| return payload; | |
| } | |
| function renderRequest(path, payload) { | |
| const request = { | |
| method: "POST", | |
| url: buildUrl(path), | |
| headers: { "Content-Type": "application/json" }, | |
| body: payload | |
| }; | |
| document.getElementById("requestOutput").textContent = JSON.stringify(request, null, 2); | |
| } | |
| function renderScore(body) { | |
| document.getElementById("scoreOutput").textContent = JSON.stringify(body, null, 2); | |
| const decision = document.getElementById("decision"); | |
| decision.textContent = body.decision || "-"; | |
| decision.className = body.decision || ""; | |
| document.getElementById("finalScore").textContent = body.finalScore ?? "-"; | |
| document.getElementById("journeyAction").textContent = body.journeyAction || "-"; | |
| document.getElementById("bureauRisk").textContent = body.bureauRisk || "-"; | |
| } | |
| async function runScorecard() { | |
| try { | |
| setStatus("scoreStatus", "Sending API request..."); | |
| const payload = readPayload(); | |
| const path = getScorecardPath(); | |
| renderRequest(path, payload); | |
| const result = await api(path, { method: "POST", body: JSON.stringify(payload) }); | |
| renderScore(result.body); | |
| setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - scorecard completed.`, "ok"); | |
| } catch (error) { | |
| setStatusState("scoreStatus", `Error: ${error.message}`, "error"); | |
| } | |
| } | |
| async function triggerAfterDueDiligence() { | |
| const dueDiligenceStatus = document.getElementById("dueDiligenceStatus").value; | |
| if (dueDiligenceStatus !== "APPROVED") { | |
| setStatusState( | |
| "scoreStatus", | |
| `API not triggered because due diligence is ${dueDiligenceStatus}. Select Approved to call the scorecard.`, | |
| "error" | |
| ); | |
| return; | |
| } | |
| document.getElementById("mode").value = "generate-scorecard"; | |
| refreshRequestPreview(); | |
| await runScorecard(); | |
| } | |
| async function getSavedResult() { | |
| try { | |
| const leadId = document.getElementById("leadId").value; | |
| setStatus("scoreStatus", "Fetching saved result..."); | |
| const result = await api(`/scorecard/${encodeURIComponent(leadId)}`); | |
| renderScore(result.body); | |
| setStatusState("scoreStatus", `HTTP ${result.status} ${result.statusText} - saved result loaded.`, "ok"); | |
| } catch (error) { | |
| setStatusState("scoreStatus", `Error: ${error.message}`, "error"); | |
| } | |
| } | |
| async function loadRules() { | |
| try { | |
| setStatus("ruleStatus", "Loading rules..."); | |
| const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default"); | |
| const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN"); | |
| const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`); | |
| const rules = result.body; | |
| const rows = rules.map(rule => ` | |
| <tr> | |
| <td>${rule.id}</td> | |
| <td>${rule.rule_code}<br><small>${rule.reason_code}</small></td> | |
| <td>${rule.rule_type}</td> | |
| <td>${rule.field_name} ${rule.operator} ${rule.field_value}</td> | |
| <td>${rule.score_value ?? ""} ${rule.decision_action ?? ""}</td> | |
| <td>${rule.is_active ? "Active" : "Disabled"}</td> | |
| <td> | |
| <button class="ghost" onclick='editRule(${JSON.stringify(rule)})'>Edit</button> | |
| <button class="${rule.is_active ? "danger" : "secondary"}" onclick="toggleRule(${rule.id}, ${!rule.is_active})"> | |
| ${rule.is_active ? "Disable" : "Enable"} | |
| </button> | |
| </td> | |
| </tr> | |
| `).join(""); | |
| document.getElementById("rulesTable").innerHTML = rows; | |
| setStatus("ruleStatus", `${rules.length} rules loaded.`); | |
| } catch (error) { | |
| setStatus("ruleStatus", `Error: ${error.message}`); | |
| } | |
| } | |
| function buildRulePayload() { | |
| const scoreValue = document.getElementById("scoreValue").value; | |
| return { | |
| tenant_id: document.getElementById("tenantId").value || "default", | |
| product_type: document.getElementById("productType").value || "TWO_WHEELER_LOAN", | |
| rule_code: document.getElementById("ruleCode").value, | |
| rule_name: document.getElementById("ruleName").value, | |
| rule_type: document.getElementById("ruleType").value, | |
| field_name: document.getElementById("fieldName").value, | |
| operator: document.getElementById("operator").value, | |
| field_value: document.getElementById("fieldValue").value, | |
| score_value: scoreValue ? Number(scoreValue) : null, | |
| score_component: document.getElementById("scoreComponent").value || null, | |
| decision_action: document.getElementById("decisionAction").value || null, | |
| reason_code: document.getElementById("reasonCode").value, | |
| priority: Number(document.getElementById("priority").value || 100), | |
| is_active: true | |
| }; | |
| } | |
| async function saveRule() { | |
| const editingRuleId = document.getElementById("editingRuleId").value; | |
| if (editingRuleId) { | |
| await updateRule(editingRuleId); | |
| } else { | |
| await createRule(); | |
| } | |
| } | |
| async function createRule() { | |
| try { | |
| setStatus("ruleStatus", "Creating rule..."); | |
| const payload = buildRulePayload(); | |
| await api("/rules", { method: "POST", body: JSON.stringify(payload) }); | |
| clearRuleForm(); | |
| await loadRules(); | |
| setStatus("ruleStatus", "Rule created."); | |
| } catch (error) { | |
| setStatus("ruleStatus", `Error: ${error.message}`); | |
| } | |
| } | |
| async function updateRule(ruleId) { | |
| try { | |
| setStatus("ruleStatus", "Updating rule..."); | |
| const payload = buildRulePayload(); | |
| delete payload.tenant_id; | |
| delete payload.product_type; | |
| delete payload.rule_code; | |
| delete payload.rule_type; | |
| payload.actor = "admin-ui"; | |
| await api(`/rules/${ruleId}`, { method: "PUT", body: JSON.stringify(payload) }); | |
| clearRuleForm(); | |
| await loadRules(); | |
| setStatus("ruleStatus", "Rule updated."); | |
| } catch (error) { | |
| setStatus("ruleStatus", `Error: ${error.message}`); | |
| } | |
| } | |
| function editRule(rule) { | |
| document.getElementById("editingRuleId").value = rule.id; | |
| document.getElementById("ruleCode").value = rule.rule_code || ""; | |
| document.getElementById("ruleName").value = rule.rule_name || ""; | |
| document.getElementById("ruleType").value = rule.rule_type || "SCORE"; | |
| document.getElementById("fieldName").value = rule.field_name || ""; | |
| document.getElementById("operator").value = rule.operator || "="; | |
| document.getElementById("fieldValue").value = rule.field_value || ""; | |
| document.getElementById("scoreValue").value = rule.score_value ?? ""; | |
| document.getElementById("scoreComponent").value = rule.score_component || ""; | |
| document.getElementById("decisionAction").value = rule.decision_action || ""; | |
| document.getElementById("reasonCode").value = rule.reason_code || ""; | |
| document.getElementById("priority").value = rule.priority || 100; | |
| document.getElementById("saveRuleButton").textContent = "Save Rule Changes"; | |
| document.getElementById("editText").textContent = `Editing #${rule.id} - ${rule.rule_code}`; | |
| document.getElementById("editBanner").style.display = "flex"; | |
| setStatus("ruleStatus", "Rule loaded into form. Change values and save."); | |
| window.scrollTo({ top: 0, behavior: "smooth" }); | |
| } | |
| async function toggleRule(id, isActive) { | |
| try { | |
| await api(`/rules/${id}/status`, { | |
| method: "PATCH", | |
| body: JSON.stringify({ is_active: isActive, actor: "admin-ui" }) | |
| }); | |
| await loadRules(); | |
| } catch (error) { | |
| setStatus("ruleStatus", `Error: ${error.message}`); | |
| } | |
| } | |
| function clearRuleForm() { | |
| [ | |
| "ruleCode", | |
| "ruleName", | |
| "fieldName", | |
| "fieldValue", | |
| "scoreValue", | |
| "scoreComponent", | |
| "reasonCode" | |
| ].forEach(id => document.getElementById(id).value = ""); | |
| document.getElementById("priority").value = "100"; | |
| cancelEdit(); | |
| } | |
| function cancelEdit() { | |
| document.getElementById("editingRuleId").value = ""; | |
| document.getElementById("saveRuleButton").textContent = "Create Rule"; | |
| document.getElementById("editBanner").style.display = "none"; | |
| } | |
| async function exportRules() { | |
| try { | |
| const tenant = encodeURIComponent(document.getElementById("tenantId").value || "default"); | |
| const product = encodeURIComponent(document.getElementById("productType").value || "TWO_WHEELER_LOAN"); | |
| const result = await api(`/rules?tenant_id=${tenant}&product_type=${product}`); | |
| const rules = result.body; | |
| const blob = new Blob([JSON.stringify(rules, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| link.download = `${product}_rules.json`; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| setStatus("ruleStatus", "Rules exported."); | |
| } catch (error) { | |
| setStatus("ruleStatus", `Error: ${error.message}`); | |
| } | |
| } | |
| function openDocs() { | |
| window.open(buildUrl("/docs"), "_blank"); | |
| } | |
| async function copyCurl() { | |
| try { | |
| const payload = readPayload(); | |
| const path = getScorecardPath(); | |
| renderRequest(path, payload); | |
| const curl = [ | |
| `curl -X POST "${buildUrl(path)}"`, | |
| `-H "Content-Type: application/json"`, | |
| `-d '${JSON.stringify(payload)}'` | |
| ].join(" "); | |
| await navigator.clipboard.writeText(curl); | |
| setStatusState("scoreStatus", "cURL copied.", "ok"); | |
| } catch (error) { | |
| setStatusState("scoreStatus", `Error: ${error.message}`, "error"); | |
| } | |
| } | |
| document.getElementById("apiBaseUrl").value = window.location.origin; | |
| document.getElementById("apiBaseUrl").addEventListener("input", refreshRequestPreview); | |
| loadSample(); | |
| loadRules(); | |
| </script> | |
| </body> | |
| </html> | |