Files
ecw-charge-validation/web/templates/base.html
T
ManikandanandClaude Opus 5.5 ce9bdbd974 Sign-in page redesign and Portal roles Super Admin / Admin / Viewer
Sign-in: brand panel, show / hide password, Caps Lock warning, moving
background, tab icon. Roles: Super Admin manages Admins and Viewers,
Admin manages Viewers only (and can Start / Stop, see Sign-ins), Viewer
sees claims and Runs only. create_tables.sql moves an old table.
71 test files, 1220 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:45:44 +05:30

244 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="run-token" content="{{ run_token }}">
<title>{% block title %}Charge Validation{% endblock %} · eCW</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2312355b'/%3E%3Ctext x='16' y='21' font-family='Segoe UI,Arial' font-size='13' font-weight='800' text-anchor='middle' fill='%23fff'%3ECV%3C/text%3E%3C/svg%3E">
{% block head %}{% endblock %}
<style>
/* Navy & blue (operator, 2026-10-19): navy header, white cards on light
grey, blue buttons; Pass green, Fail red, Review amber everywhere. */
:root {
--navy: #12355b; --navy-2: #0f2a47; --bg: #f4f6fa; --panel: #ffffff;
--text: #1f2a37; --muted: #6b7785; --line: #dde3ec; --head: #e8edf4;
--accent: #1565c0; --accent-dark: #0d4f9e;
--pass: #2e7d32; --pass-bg: #e8f5e9;
--fail: #c62828; --fail-bg: #fdecea;
--review: #b45309; --review-bg: #fff3e0; --review-bar: #ef6c00;
--stripe: #f9fafc; --hilite: #eef4ff; --sel: #dbe7fb;
--shadow: 0 1px 2px rgba(18, 53, 91, .06), 0 2px 8px rgba(18, 53, 91, .05);
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 14px/1.45 "Segoe UI", system-ui, sans-serif;
display: flex; flex-direction: column; min-height: 100vh; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* Header, breadcrumb, page and footer share one column (1180px), so
their edges line up (operator, 2026-10-26). */
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; width: 100%; }
header.top { background: var(--navy); color: #fff; }
header.top .wrap { display: flex; align-items: center; gap: 14px;
min-height: 58px; }
header.top .brand { color: #fff; font-size: 17px; font-weight: 600;
letter-spacing: .2px; display: flex; align-items: center; }
header.top .brand:hover { text-decoration: none; }
header.top .mark { display: inline-grid; place-items: center; width: 30px;
height: 30px; border-radius: 8px; background: #fff;
color: var(--navy); font-size: 12px; font-weight: 800;
margin-right: 10px; letter-spacing: .5px; }
.pill { border-radius: 12px; padding: 2px 10px; font-size: 11px;
font-weight: 700; letter-spacing: .4px; background: var(--pass);
color: #fff; }
.pill.LIVE-WRITE { background: var(--fail); }
header.top nav.links { margin-left: auto; display: flex; gap: 4px;
align-items: center; }
header.top nav.links a { color: #e3ecf7; font-weight: 600; padding: 6px 11px;
border-radius: 6px; transition: background .15s; }
header.top nav.links a:hover { background: rgba(255, 255, 255, .08);
text-decoration: none; }
header.top nav.links a.on { background: rgba(255, 255, 255, .16); color: #fff; }
header.top .who { color: #b9c6d6; font-size: 13px; display: flex;
align-items: center; gap: 7px; margin-left: 10px; }
header.top .who b { color: #fff; }
header.top .avatar { display: inline-grid; place-items: center; width: 26px;
height: 26px; border-radius: 50%; background: var(--accent);
color: #fff; font-size: 12px; font-weight: 700; }
header.top form.out { margin: 0; }
header.top form.out button { background: transparent; border-color: #6f86a3;
font-weight: 400; padding: 4px 10px; }
.msg { border-radius: 8px; padding: 10px 14px; margin-bottom: 14px; }
.msg.bad { background: var(--fail-bg); color: var(--fail); }
.msg.good { background: var(--pass-bg); color: var(--pass); }
nav.crumbs { background: var(--navy-2); color: #b9c6d6; padding: 7px 0;
font-size: 13px; }
nav.crumbs a { color: #e3ecf7; }
nav.crumbs .sep { margin: 0 8px; color: #7d8ea3; }
nav.crumbs .here { color: #fff; font-weight: 600; }
main { flex: 1; width: 100%; max-width: 1180px; margin: 0 auto;
padding: 22px 24px 40px; }
footer.foot { background: var(--panel); border-top: 1px solid var(--line);
color: var(--muted); font-size: 12px; }
footer.foot .wrap { display: flex; gap: 16px; align-items: center;
justify-content: space-between; flex-wrap: wrap;
padding-top: 12px; padding-bottom: 12px; }
footer.foot b { color: var(--text); font-weight: 600; }
/* Motion: short, never in the way, off when Windows asks for less. */
/* Movement only, never opacity: a tab the browser holds in the
background must not leave the page faded. */
@keyframes rise { from { transform: translateY(8px); }
to { transform: none; } }
.card, .tile { animation: rise .35s ease both; }
.tiles > :nth-child(2) { animation-delay: .06s; }
.tiles > :nth-child(3) { animation-delay: .12s; }
.tiles > :nth-child(4) { animation-delay: .18s; }
a.tile, a.card { transition: transform .15s, box-shadow .15s,
border-color .15s; }
a.tile:hover, a.card:hover { transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(18, 53, 91, .12); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important;
transition: none !important; }
}
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: var(--muted); margin: 0 0 16px; }
.card { background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; padding: 16px 18px; box-shadow: var(--shadow);
margin-bottom: 18px; }
.card h2 { font-size: 15px; margin: 0 0 10px; }
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px; margin-bottom: 18px; }
.tile { background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; padding: 12px 16px; box-shadow: var(--shadow);
color: var(--text); display: block; }
.tile .n { font-size: 26px; font-weight: 700; display: block; }
.tile .l { color: var(--muted); font-size: 12px; text-transform: uppercase;
letter-spacing: .5px; }
.tile.Pass .n { color: var(--pass); }
.tile.Fail .n { color: var(--fail); }
.tile.Review .n { color: var(--review); }
a.tile:hover { text-decoration: none; border-color: var(--accent); }
a.tile.on { border: 2px solid var(--accent); padding: 11px 15px; }
select, input, button { font: inherit; padding: 7px 10px;
border: 1px solid #c9d2de; border-radius: 6px;
background: var(--panel); color: var(--text); }
button, .btn { background: var(--accent); color: #fff;
border: 1px solid var(--accent); border-radius: 6px;
padding: 7px 14px; cursor: pointer; font-weight: 600;
display: inline-block; }
button:hover, .btn:hover { background: var(--accent-dark);
text-decoration: none; }
button.plain { background: var(--panel); color: var(--text);
border-color: #c9d2de; font-weight: 400; }
button.plain:hover { background: var(--hilite); }
button[disabled] { opacity: .5; cursor: default; }
.cat { display: inline-block; padding: 2px 9px; border-radius: 10px;
font-weight: 600; font-size: 12px; }
.cat.Pass { background: var(--pass-bg); color: var(--pass); }
.cat.Fail { background: var(--fail-bg); color: var(--fail); }
.cat.Review { background: var(--review-bg); color: var(--review); }
.empty { background: var(--panel); border: 1px dashed #c9d2de;
border-radius: 10px; padding: 26px; color: var(--muted);
text-align: center; }
.note { color: var(--muted); font-size: 12px; }
/* Tables: short, no sideways scroll; long text cut on one line. */
table.list { width: 100%; border-collapse: separate; border-spacing: 0;
background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; overflow: hidden; table-layout: fixed; }
table.list th, table.list td { padding: 9px 12px; text-align: left;
border-bottom: 1px solid var(--line);
overflow: hidden; text-overflow: ellipsis;
white-space: nowrap; }
table.list th { background: var(--head); font-size: 12px; color: #3b4757;
font-weight: 600; text-transform: uppercase;
letter-spacing: .4px; }
table.list tbody tr { cursor: pointer; }
table.list tbody tr:nth-child(even) td { background: var(--stripe); }
table.list tbody tr:hover td { background: var(--hilite); }
table.list tbody tr.sel td { background: var(--sel); }
table.list tbody tr:last-child td { border-bottom: 0; }
td.claim { font-weight: 600; }
.pager { display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
margin: 12px 0; }
.pager button { padding: 4px 10px; background: var(--panel);
color: var(--text); border-color: #c9d2de; font-weight: 400; }
.pager button.cur { background: var(--accent); color: #fff;
border-color: var(--accent); }
.pager button[disabled]:not(.cur) { opacity: .4; }
.pager .info { color: var(--muted); margin-left: 8px; }
form.search { display: flex; gap: 8px; align-items: center;
flex-wrap: wrap; }
form.search input { min-width: 220px; }
/* Side panels. The page under an open panel does not scroll: one scroll
bar, the panel's (operator, 2026-10-15; review 2026-10-19). */
body.drawer-open { overflow: hidden; }
.drawer { position: fixed; top: 0; right: 0; height: 100vh;
width: min(480px, 100vw); background: var(--panel);
border-left: 1px solid var(--line); z-index: 20;
box-shadow: -8px 0 24px rgba(18, 53, 91, .12);
padding: 0 0 30px; overflow-y: auto; }
.drawer[hidden] { display: none; }
.drawer .top { display: flex; align-items: center; gap: 10px;
background: var(--navy); color: #fff; padding: 12px 18px;
position: sticky; top: 0; }
.drawer .top h2 { font-size: 16px; margin: 0; }
.drawer .top .close { margin-left: auto; background: transparent;
border-color: #6f86a3; font-weight: 400; }
.drawer .body { padding: 14px 18px; }
.drawer dl { display: grid; grid-template-columns: 140px 1fr;
gap: 6px 12px; margin: 0; font-size: 13px; }
.drawer dt { color: var(--muted); }
.drawer dd { margin: 0; white-space: pre-wrap; word-break: break-word; }
.drawer h3 { font-size: 14px; margin: 16px 0 6px; }
.drawer pre { background: var(--head); padding: 10px; border-radius: 6px;
white-space: pre-wrap; font-size: 12px; margin: 0; }
@media (max-width: 760px) {
.tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
main { padding: 16px; }
.wrap { padding-left: 16px; padding-right: 16px; }
header.top .wrap { flex-wrap: wrap; padding-top: 8px; padding-bottom: 8px; }
header.top nav.links { margin-left: 0; flex-wrap: wrap; }
}
{% block style %}{% endblock %}
</style>
<script>
// Shared by every page, defined before any page script runs: one side
// panel open at a time; Escape and every Close button shut it.
const $ = id => document.getElementById(id);
const CATEGORIES = {{ categories | list | tojson }};
function openPanel(id) {
document.querySelectorAll('.drawer').forEach(p => p.hidden = p.id !== id);
document.body.classList.toggle('drawer-open', id !== null);
}
const closePanels = () => openPanel(null);
document.addEventListener('click', e => {
if (e.target.closest('.drawer .close')) closePanels();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closePanels();
});
</script>
</head>
<body>
{% set section = crumbs[1][0] if crumbs | length > 1 else crumbs[0][0] %}
<header class="top">
<div class="wrap">
<a class="brand" href="/"><span class="mark" aria-hidden="true">CV</span>eCW Charge Validation</a>
<span class="pill {{ mode }}" title="SAVE_CORRECTIONS mode">{{ mode }}</span>
{% if user %}<nav class="links">
{% for label, link, admin in [("Dashboard", "/", False), ("Claim status", "/days", False), ("Runs", "/runs", False), ("Users", "/admin/users", True), ("Sign-ins", "/admin/signins", True)] %}{% if not admin or is_admin %}{% if label == section %}<a href="{{ link }}" class="on" aria-current="page">{{ label }}</a>{% else %}<a href="{{ link }}">{{ label }}</a>{% endif %}{% endif %}{% endfor %}
<a class="who" href="/account/password" title="Change password"><span class="avatar" aria-hidden="true">{{ (user.display_name or "?")[:1] | upper }}</span><b>{{ user.display_name }}</b> · {{ user.role }}</a>
<form class="out" method="post" action="/signout"><input type="hidden" name="token" value="{{ run_token }}"><button type="submit">Sign out</button></form>
</nav>{% endif %}
</div>
</header>
<nav class="crumbs">
<div class="wrap">{% for label, link in crumbs %}{% if not loop.first %}<span class="sep">›</span>{% endif %}{% if link %}<a href="{{ link }}">{{ label }}</a>{% else %}<span class="here">{{ label }}</span>{% endif %}{% endfor %}</div>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer class="foot">
<div class="wrap">
<span><b>eCW Charge Validation</b> · Ecare India</span>
<span class="phi">Local page — contains patient data; do not share
screenshots outside the team.</span>
<span>Mode <b>{{ mode }}</b></span>
</div>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>