Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

Commit 08bfc7e

Browse files
committed
webui: improved layout, with example queries
1 parent 2dab7b0 commit 08bfc7e

3 files changed

Lines changed: 422 additions & 52 deletions

File tree

webui/index.html.tmpl

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,39 @@
33
<head>
44
<title>Rail Message SQL Viewer</title>
55
<link rel="stylesheet" href="{{ .BaseURL }}static/style.css">
6-
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover">
6+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
77
</head>
88
<body>
99
<header>
1010
<h1>Rail Message SQL Viewer</h1>
1111
</header>
1212

13-
<main class="list">
14-
<p>
15-
Searching Files from {{ yyyymmdd .TimeRangeMin }} to {{ yyyymmdd .TimeRangeMax }}.
16-
</p>
13+
<div class="container">
14+
<main class="list">
15+
<div class="date-nav">
16+
<span id="date-range">Searching Files from {{ yyyymmdd .TimeRangeMin }} to {{ yyyymmdd .TimeRangeMax }}</span>
17+
<nav class="pagination">
18+
<a href="#" id="older-link" data-url="{{ startDateParam .TimeRangeMin }}"><- Older</a>
19+
<a href="#" id="newer-link" data-url="{{ endDateParam .TimeRangeMax }}">Newer -></a>
20+
</nav>
21+
</div>
1722

18-
<nav class="pagination">
19-
<a href="{{ startDateParam .TimeRangeMin }}"><- Older</a>
20-
<a href="{{ endDateParam .TimeRangeMax }}">Newer -></a>
21-
</nav>
23+
<form id="search-form">
24+
<textarea id="query" rows="12" cols="100" placeholder="Enter search query..."></textarea>
25+
<br />
26+
<input type="submit" value="Search..." />
27+
</form>
2228

23-
<form id="search-form">
24-
<textarea id="query" rows="12" cols="100" placeholder="Enter search query..."></textarea>
25-
<br />
26-
<input type="submit" value="Search..." />
27-
</form>
29+
<p id="error"></p>
2830

29-
<p id="error"></p>
31+
<table id="results"></table>
32+
</main>
3033

31-
<table id="results"></table>
32-
</main>
34+
<aside class="sidebar">
35+
<h2>Predefined Queries</h2>
36+
<div id="accordion-container"></div>
37+
</aside>
38+
</div>
3339

3440
<script src="{{ .BaseURL }}static/index.js"></script>
3541
</body>

webui/static/index.js

Lines changed: 226 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,282 @@
1-
window.onload = function() {
2-
var searchForm = document.querySelector("#search-form");
1+
// Predefined queries data structure
2+
const predefinedQueries = [
3+
{
4+
category: "Files",
5+
queries: [
6+
{ name: "Recent Files", query: `
7+
SELECT
8+
file_id, filename, file_creation_date as creation_date, file_creation_time as creation_time,
9+
immediate_destination_name as destination, immediate_origin_name as origin ,
10+
total_debit_entry_dollar_amount as debits, total_credit_entry_dollar_amount as credits
311
4-
searchForm.addEventListener('submit', function(event) {
12+
FROM ach_files
13+
WHERE created_at >= datetime('now', '-7 days')
14+
ORDER BY created_at DESC
15+
LIMIT 5;
16+
`},
17+
],
18+
},
19+
{
20+
category: "Entries",
21+
queries: [
22+
{ name: "Entries By Name or Account Number", query: `
23+
SELECT
24+
-- e.entry_id, e.file_id,
25+
e.transaction_code, e.amount,
26+
e.dfi_account_number, e.individual_name,
27+
e.trace_number, f.filename, f.file_creation_date
28+
29+
FROM ach_entries e
30+
JOIN ach_files f ON e.file_id = f.file_id
31+
32+
WHERE
33+
e.individual_name LIKE '%'
34+
-- e.dfi_account_number LIKE '%1234'
35+
AND created_at >= datetime('now', '-7 days')
36+
37+
ORDER BY f.created_at DESC
38+
LIMIT 10;
39+
` },
40+
],
41+
},
42+
{
43+
category: "Exceptions",
44+
queries: [
45+
{ name: "Recent Returns and Corrections", query: `
46+
SELECT
47+
-- e.entry_id,
48+
e.individual_name, e.dfi_account_number, e.amount,
49+
e.trace_number, a.return_code,
50+
-- a.payment_related_information,
51+
f.filename, f.file_creation_date
52+
53+
FROM ach_entries e
54+
JOIN ach_addendas a ON e.entry_id = a.entry_id AND e.file_id = a.file_id
55+
JOIN ach_files f ON e.file_id = f.file_id
56+
57+
WHERE a.return_code IS NOT NULL OR a.change_code IS NOT NULL
58+
59+
ORDER BY f.created_at DESC
60+
LIMIT 10;
61+
`},
62+
],
63+
},
64+
];
65+
66+
// Function to format date as YYYY-MM-DD
67+
function yyyymmdd(date) {
68+
if (!date || isNaN(date)) return "Unknown";
69+
const year = date.getFullYear();
70+
const month = String(date.getMonth() + 1).padStart(2, "0");
71+
const day = String(date.getDate()).padStart(2, "0");
72+
return `${year}-${month}-${day}`;
73+
}
74+
75+
// Function to calculate new date ranges for pagination
76+
function calculateDateRangeUrls() {
77+
const params = new URLSearchParams(window.location.search);
78+
let startDate = params.get("startDate") ? new Date(params.get("startDate")) : new Date();
79+
let endDate = params.get("endDate") ? new Date(params.get("endDate")) : new Date();
80+
81+
// Default to a 7-day range if dates are invalid
82+
if (isNaN(startDate) || isNaN(endDate)) {
83+
endDate = new Date();
84+
startDate = new Date();
85+
startDate.setDate(endDate.getDate() - 7);
86+
}
87+
88+
// Calculate "Older" range (shift back 7 days)
89+
const olderStart = new Date(startDate);
90+
olderStart.setDate(startDate.getDate() - 7);
91+
const olderEnd = new Date(endDate);
92+
olderEnd.setDate(endDate.getDate() - 7);
93+
94+
// Calculate "Newer" range (shift forward 7 days)
95+
const newerStart = new Date(startDate);
96+
newerStart.setDate(startDate.getDate() + 7);
97+
const newerEnd = new Date(endDate);
98+
newerEnd.setDate(endDate.getDate() + 7);
99+
100+
// Generate URLs
101+
const olderUrl = `./?startDate=${yyyymmdd(olderStart)}&endDate=${yyyymmdd(olderEnd)}`;
102+
const newerUrl = `./?startDate=${yyyymmdd(newerStart)}&endDate=${yyyymmdd(newerEnd)}`;
103+
104+
return { olderUrl, newerUrl };
105+
}
106+
107+
// Function to update date range text and pagination URLs
108+
function updateDateRangeAndLinks() {
109+
const params = new URLSearchParams(window.location.search);
110+
const startDate = params.get("startDate") ? new Date(params.get("startDate")) : new Date();
111+
const endDate = params.get("endDate") ? new Date(params.get("endDate")) : new Date();
112+
const dateRangeElement = document.querySelector("#date-range");
113+
114+
dateRangeElement.textContent = `Searching Files from ${yyyymmdd(startDate)} to ${yyyymmdd(endDate)}`;
115+
116+
// Update pagination link URLs
117+
const { olderUrl, newerUrl } = calculateDateRangeUrls();
118+
document.querySelector("#older-link").setAttribute("data-url", olderUrl);
119+
document.querySelector("#newer-link").setAttribute("data-url", newerUrl);
120+
}
121+
122+
// Function to populate the accordion
123+
function populateAccordion() {
124+
const accordionContainer = document.querySelector("#accordion-container");
125+
126+
predefinedQueries.forEach((category, index) => {
127+
const accordionItem = document.createElement("div");
128+
accordionItem.classList.add("accordion-item");
129+
130+
const header = document.createElement("button");
131+
header.classList.add("accordion-header");
132+
header.textContent = category.category;
133+
header.setAttribute("aria-expanded", "false");
134+
header.setAttribute("aria-controls", `accordion-content-${index}`);
135+
136+
const content = document.createElement("div");
137+
content.classList.add("accordion-content");
138+
content.id = `accordion-content-${index}`;
139+
140+
category.queries.forEach((query) => {
141+
const option = document.createElement("button");
142+
option.classList.add("query-option");
143+
option.textContent = query.name;
144+
option.setAttribute("data-query", query.query);
145+
option.addEventListener("click", () => {
146+
document.querySelector("#query").value = query.query;
147+
});
148+
content.appendChild(option);
149+
});
150+
151+
header.addEventListener("click", () => {
152+
const isActive = content.classList.contains("active");
153+
document.querySelectorAll(".accordion-content").forEach((c) => {
154+
c.classList.remove("active");
155+
c.previousElementSibling.setAttribute("aria-expanded", "false");
156+
});
157+
if (!isActive) {
158+
content.classList.add("active");
159+
header.setAttribute("aria-expanded", "true");
160+
}
161+
});
162+
163+
accordionItem.appendChild(header);
164+
accordionItem.appendChild(content);
165+
accordionContainer.appendChild(accordionItem);
166+
});
167+
}
168+
169+
// Main initialization
170+
window.onload = function () {
171+
// Populate the accordion
172+
populateAccordion();
173+
174+
// Update date range text and pagination URLs on load
175+
updateDateRangeAndLinks();
176+
177+
// Handle pagination link clicks
178+
const olderLink = document.querySelector("#older-link");
179+
const newerLink = document.querySelector("#newer-link");
180+
181+
olderLink.addEventListener("click", (event) => {
182+
event.preventDefault();
183+
const newUrl = olderLink.getAttribute("data-url");
184+
window.history.pushState({ startDate: new URLSearchParams(newUrl).get("startDate"), endDate: new URLSearchParams(newUrl).get("endDate") }, "", newUrl);
185+
updateDateRangeAndLinks();
186+
});
187+
188+
newerLink.addEventListener("click", (event) => {
5189
event.preventDefault();
190+
const newUrl = newerLink.getAttribute("data-url");
191+
window.history.pushState({ startDate: new URLSearchParams(newUrl).get("startDate"), endDate: new URLSearchParams(newUrl).get("endDate") }, "", newUrl);
192+
updateDateRangeAndLinks();
193+
});
6194

7-
var query = document.querySelector("#query");
195+
// Handle browser back/forward navigation
196+
window.addEventListener("popstate", () => {
197+
updateDateRangeAndLinks();
198+
});
199+
200+
// Handle search form submission
201+
const searchForm = document.querySelector("#search-form");
202+
searchForm.addEventListener("submit", function (event) {
203+
event.preventDefault();
204+
const query = document.querySelector("#query");
8205
performSearch(query.value);
9206
});
10207
};
11208

12209
function performSearch(body) {
13210
const currentParams = new URLSearchParams(window.location.search);
14-
15211
const queryParams = new URLSearchParams();
16-
var startDate = currentParams.get('startDate');
212+
const startDate = currentParams.get("startDate");
17213
if (startDate != null) {
18214
queryParams.set("startDate", startDate);
19215
}
20-
var endDate = currentParams.get('endDate');
216+
const endDate = currentParams.get("endDate");
21217
if (endDate != null) {
22218
queryParams.set("endDate", endDate);
23219
}
24220

221+
// Clear error message before search
222+
const errorElm = document.querySelector("#error");
223+
if (errorElm) {
224+
errorElm.textContent = "";
225+
}
226+
25227
fetch(`./search?${queryParams.toString()}`, {
26-
method: 'POST',
228+
method: "POST",
27229
headers: {
28-
'Content-Type': 'text/plain'
230+
"Content-Type": "text/plain",
29231
},
30232
body: body,
31233
})
32-
.then(response => response.json())
33-
.then(data => {
34-
populateSearchResponse(data)
234+
.then((response) => response.json())
235+
.then((data) => {
236+
populateSearchResponse(data);
35237
})
36-
.catch(error => {
37-
var elm = document.querySelector("#error");
238+
.catch((error) => {
239+
const elm = document.querySelector("#error");
38240
if (elm) {
39241
elm.textContent = error.message;
40242
} else {
41-
console.error('Error element (#error) not found');
243+
console.error("Error element (#error) not found");
42244
}
43-
console.error('Fetch error:', error);
245+
console.error("Fetch error:", error);
44246
});
45247
}
46248

47249
function populateSearchResponse(data) {
48-
var table = document.querySelector("#results");
49-
table.innerHTML = ''; // Clear the previous rows
250+
const table = document.querySelector("#results");
251+
table.innerHTML = ""; // Clear the previous rows
50252

51253
// Add the Headers
52-
const headers = document.createElement("tr"); // Fixed typo
254+
const headers = document.createElement("tr");
53255
if (data.Headers) {
54-
data.Headers.Columns.forEach(col => {
55-
const th = document.createElement("th"); // Use <th> for headers
256+
data.Headers.Columns.forEach((col) => {
257+
const th = document.createElement("th");
56258
th.innerHTML = col;
57-
58259
headers.append(th);
59260
});
60261
table.append(headers);
61262
}
62263

63264
// Add the rows
64265
if (data.Rows) {
65-
data.Rows.forEach(r => {
266+
data.Rows.forEach((r) => {
66267
const row = document.createElement("tr");
67-
68-
r.Columns.forEach(col => {
268+
r.Columns.forEach((col) => {
69269
const td = document.createElement("td");
70270
td.innerHTML = col;
71-
72271
row.append(td);
73272
});
74-
75273
table.append(row);
76274
});
77275
}
78276

79277
// Show an error if one exists
80278
if (data.error) {
81-
var elm = document.querySelector("#error");
279+
const elm = document.querySelector("#error");
82280
elm.textContent = data.error;
83281
}
84282
}

0 commit comments

Comments
 (0)