482 lines
13 KiB
Plaintext
482 lines
13 KiB
Plaintext
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
|
<style>
|
|
.banking-grid {
|
|
display: grid;
|
|
gap: 16px;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
}
|
|
.banking-card {
|
|
padding: 14px;
|
|
border-radius: 14px;
|
|
background: var(--surface-2);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.banking-label {
|
|
color: var(--ink-soft);
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
.banking-value {
|
|
font-size: 1.4rem;
|
|
font-weight: 700;
|
|
}
|
|
.banking-currency {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.banking-currency img {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
object-fit: cover;
|
|
}
|
|
.funds-grid {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
.fund-item {
|
|
background: var(--surface-2);
|
|
border-radius: 14px;
|
|
padding: 12px;
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
.fund-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.fund-progress {
|
|
font-size: 0.9rem;
|
|
color: var(--ink-soft);
|
|
}
|
|
.uuid-chip {
|
|
border: 1px solid var(--border);
|
|
background: transparent;
|
|
color: inherit;
|
|
border-radius: 8px;
|
|
padding: 2px 8px;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
}
|
|
.uuid-chip:hover {
|
|
border-color: var(--ink-soft);
|
|
}
|
|
.user-search {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
.user-results {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
.user-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
padding: 6px 10px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface-2);
|
|
}
|
|
.user-row.is-selected {
|
|
border-color: var(--accent);
|
|
}
|
|
.user-main {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.user-name {
|
|
font-weight: 600;
|
|
}
|
|
.user-pills {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.user-pill {
|
|
font-size: 0.7rem;
|
|
padding: 2px 6px;
|
|
border-radius: 999px;
|
|
background: var(--surface-3);
|
|
color: var(--ink-soft);
|
|
}
|
|
.user-expand {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--ink-soft);
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
}
|
|
.user-details {
|
|
display: none;
|
|
font-size: 0.78rem;
|
|
color: var(--ink-soft);
|
|
margin-top: 4px;
|
|
}
|
|
.user-row.is-open .user-details {
|
|
display: inline;
|
|
}
|
|
.user-select {
|
|
background: var(--accent);
|
|
color: var(--accent-ink);
|
|
border: none;
|
|
border-radius: 999px;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
}
|
|
.tx-note-details {
|
|
margin: 0;
|
|
}
|
|
.tx-note-details summary {
|
|
cursor: pointer;
|
|
color: var(--ink);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.tx-note-details ul {
|
|
margin: 6px 0 0;
|
|
padding-left: 18px;
|
|
color: var(--ink-soft);
|
|
}
|
|
.tx-note-period {
|
|
margin-top: 6px;
|
|
color: var(--ink-soft);
|
|
font-size: 0.85rem;
|
|
}
|
|
</style>
|
|
|
|
<section class="card">
|
|
<div class="section-header">
|
|
<div>
|
|
<h1><%= config.banking.label %></h1>
|
|
<p class="hint">Review balances, transfer funds, and track your transaction history.</p>
|
|
</div>
|
|
<div class="banking-currency">
|
|
<% if (config.currency.icon) { %>
|
|
<img src="<%= config.currency.icon %>" alt="Currency icon" />
|
|
<% } %>
|
|
<strong><%= config.currency.name %></strong>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Account snapshot</h2>
|
|
<div class="banking-grid">
|
|
<div class="banking-card">
|
|
<span class="banking-label">Current balance</span>
|
|
<span class="banking-value"><%= userStats.balance %></span>
|
|
</div>
|
|
<div class="banking-card">
|
|
<span class="banking-label">Total earned</span>
|
|
<span class="banking-value"><%= userStats.totalEarned %></span>
|
|
</div>
|
|
<div class="banking-card">
|
|
<span class="banking-label">Total spent</span>
|
|
<span class="banking-value"><%= userStats.totalSpent %></span>
|
|
</div>
|
|
<div class="banking-card">
|
|
<span class="banking-label">Sent to others</span>
|
|
<span class="banking-value"><%= userStats.totalSent %></span>
|
|
</div>
|
|
<div class="banking-card">
|
|
<span class="banking-label">Received from others</span>
|
|
<span class="banking-value"><%= userStats.totalReceived %></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Transfer to another user</h2>
|
|
<form method="post" action="/profile/banking/transfer" class="form-grid">
|
|
<div class="field user-search">
|
|
<label>Recipient username</label>
|
|
<input name="username" id="banking-username" placeholder="Search by username or linked account" autocomplete="off" />
|
|
<div class="user-results" id="banking-results"></div>
|
|
<span class="hint">Matches show the platform(s) where the username appears. Expand to see linked accounts.</span>
|
|
</div>
|
|
<div class="field">
|
|
<label>Amount</label>
|
|
<input name="amount" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Note (optional)</label>
|
|
<input name="note" />
|
|
</div>
|
|
<button type="submit" class="button">Send transfer</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2><%= config.communityFunds.plural %></h2>
|
|
<p class="hint">Support shared community goals with direct deposits.</p>
|
|
</div>
|
|
</div>
|
|
<% if (!funds.length) { %>
|
|
<p>No <%= config.communityFunds.plural.toLowerCase() %> are active right now.</p>
|
|
<% } else { %>
|
|
<div class="funds-grid">
|
|
<% funds.forEach((fund) => { %>
|
|
<div class="fund-item">
|
|
<div class="fund-meta">
|
|
<strong><%= fund.name %></strong>
|
|
<span class="fund-progress"><%= fund.current_amount %>/<%= fund.target_amount %></span>
|
|
</div>
|
|
<span class="hint"><%= fund.description || '' %></span>
|
|
<form method="post" action="/profile/banking/funds/<%= fund.id %>/donate" class="form-grid">
|
|
<div class="field">
|
|
<label>Amount</label>
|
|
<input name="amount" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Note (optional)</label>
|
|
<input name="note" />
|
|
</div>
|
|
<button type="submit" class="button subtle">Donate</button>
|
|
</form>
|
|
</div>
|
|
<% }) %>
|
|
</div>
|
|
<% } %>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<div class="section-header">
|
|
<div>
|
|
<h2>Transaction history</h2>
|
|
<p class="hint">All account activity with UUID records.</p>
|
|
</div>
|
|
<div class="table-tools">
|
|
<input
|
|
class="table-search"
|
|
type="search"
|
|
placeholder="Search transactions"
|
|
aria-label="Search transactions"
|
|
data-table-filter="banking-transactions"
|
|
/>
|
|
<div class="table-controls">
|
|
<label class="table-page-size">
|
|
Show
|
|
<select data-table-size="banking-transactions">
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
<option value="250">250</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table
|
|
class="table"
|
|
data-table="banking-transactions"
|
|
data-pageable="true"
|
|
data-page-size="25"
|
|
data-page-sizes="25,50,100,250"
|
|
>
|
|
<thead>
|
|
<tr>
|
|
<th data-sort="id">UUID</th>
|
|
<th data-sort="type">Type</th>
|
|
<th data-sort="amount">Amount</th>
|
|
<th data-sort="from">From</th>
|
|
<th data-sort="to">To</th>
|
|
<th>Note</th>
|
|
<th data-sort="date">Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<% transactions.forEach((tx) => { %>
|
|
<% const fromName = tx.from_name || 'System'; %>
|
|
<% const toName = tx.to_name || 'System'; %>
|
|
<tr
|
|
data-search="<%= `${tx.id} ${tx.type} ${tx.amount} ${fromName} ${toName} ${tx.note_search || tx.note || ''}` %>"
|
|
data-id="<%= tx.id %>"
|
|
data-type="<%= tx.type %>"
|
|
data-amount="<%= tx.amount %>"
|
|
data-from="<%= fromName %>"
|
|
data-to="<%= toName %>"
|
|
data-date="<%= tx.created_at %>"
|
|
>
|
|
<td>
|
|
<button type="button" class="uuid-chip" data-copy="<%= tx.id %>" title="Copy UUID">
|
|
<%= tx.id %>
|
|
</button>
|
|
</td>
|
|
<td><%= tx.type %></td>
|
|
<td><%= tx.amount %></td>
|
|
<td><%= fromName %></td>
|
|
<td><%= toName %></td>
|
|
<td>
|
|
<% if (tx.activity_reward) { %>
|
|
<details class="tx-note-details">
|
|
<summary><%= tx.note_display %></summary>
|
|
<% if (tx.activity_reward.hourStart && tx.activity_reward.hourEnd) { %>
|
|
<div class="tx-note-period">
|
|
<%= new Date(tx.activity_reward.hourStart).toLocaleString() %> -
|
|
<%= new Date(tx.activity_reward.hourEnd).toLocaleString() %>
|
|
</div>
|
|
<% } %>
|
|
<ul>
|
|
<% tx.activity_reward.rewards.forEach((reward) => { %>
|
|
<li>
|
|
<%= reward.label %>: <%= reward.amount %>
|
|
<% if (reward.hits > 0) { %> (<%= reward.hits %> events)<% } %>
|
|
<% if (reward.minutes > 0) { %> (<%= reward.minutes %> min)<% } %>
|
|
</li>
|
|
<% }) %>
|
|
</ul>
|
|
</details>
|
|
<% } else { %>
|
|
<%= tx.note_display || tx.note || '-' %>
|
|
<% } %>
|
|
</td>
|
|
<td><%= new Date(tx.created_at).toLocaleString() %></td>
|
|
</tr>
|
|
<% }) %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="table-pagination" data-table-pagination="banking-transactions">
|
|
<button type="button" class="button subtle" data-page-prev>Previous</button>
|
|
<span class="table-page-label" data-page-label>Page 1 of 1</span>
|
|
<button type="button" class="button subtle" data-page-next>Next</button>
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
(() => {
|
|
const users = <%- JSON.stringify(userDirectory || []) %>;
|
|
const input = document.getElementById("banking-username");
|
|
const results = document.getElementById("banking-results");
|
|
if (!input || !results) {
|
|
return;
|
|
}
|
|
|
|
const normalize = (value) => (value || "").toString().toLowerCase();
|
|
|
|
const buildMatch = (user, term) => {
|
|
const internal = user.internal || "";
|
|
const internalMatch = normalize(internal).includes(term);
|
|
const identityMatches = (user.identities || []).filter((identity) =>
|
|
normalize(identity.display).includes(term)
|
|
);
|
|
|
|
if (!term) {
|
|
return null;
|
|
}
|
|
|
|
const display =
|
|
internalMatch ? internal : identityMatches[0]?.display || internal;
|
|
const pills = [];
|
|
if (internalMatch) {
|
|
pills.push("Internal");
|
|
}
|
|
identityMatches.forEach((identity) => {
|
|
if (!pills.includes(identity.label)) {
|
|
pills.push(identity.label);
|
|
}
|
|
});
|
|
|
|
return {
|
|
id: user.id,
|
|
display,
|
|
internal,
|
|
pills,
|
|
identities: user.identities || []
|
|
};
|
|
};
|
|
|
|
const renderResults = (term) => {
|
|
results.innerHTML = "";
|
|
if (!term) {
|
|
return;
|
|
}
|
|
const matches = users
|
|
.map((user) => buildMatch(user, term))
|
|
.filter(Boolean)
|
|
.slice(0, 8);
|
|
|
|
if (!matches.length) {
|
|
return;
|
|
}
|
|
|
|
matches.forEach((match) => {
|
|
const row = document.createElement("div");
|
|
row.className = "user-row";
|
|
row.dataset.userId = match.id;
|
|
const details = match.identities
|
|
.map(
|
|
(identity) =>
|
|
`<span>${identity.label}: ${identity.display}</span>`
|
|
)
|
|
.join(" · ");
|
|
const displayText = match.display || match.internal || "";
|
|
row.innerHTML = `
|
|
<div class="user-main">
|
|
<span class="user-name">${displayText}</span>
|
|
<div class="user-pills">
|
|
${match.pills.map((pill) => `<span class="user-pill">${pill}</span>`).join("")}
|
|
</div>
|
|
${
|
|
match.identities.length
|
|
? `<button type="button" class="user-expand">Linked</button>`
|
|
: ""
|
|
}
|
|
<span class="user-details">${details}</span>
|
|
</div>
|
|
<button type="button" class="user-select">Select</button>
|
|
`;
|
|
results.appendChild(row);
|
|
});
|
|
};
|
|
|
|
const setSelected = (value) => {
|
|
input.value = value;
|
|
results.querySelectorAll(".user-row").forEach((row) => {
|
|
row.classList.toggle(
|
|
"is-selected",
|
|
row.querySelector(".user-name")?.textContent === value
|
|
);
|
|
});
|
|
};
|
|
|
|
input.addEventListener("input", () => {
|
|
renderResults(normalize(input.value));
|
|
});
|
|
|
|
results.addEventListener("click", (event) => {
|
|
const row = event.target.closest(".user-row");
|
|
if (!row) {
|
|
return;
|
|
}
|
|
if (event.target.closest(".user-expand")) {
|
|
row.classList.toggle("is-open");
|
|
return;
|
|
}
|
|
if (event.target.closest(".user-select")) {
|
|
const name = row.querySelector(".user-name")?.textContent?.trim();
|
|
if (name) {
|
|
setSelected(name);
|
|
results.innerHTML = "";
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<%- include("../../../src/web/views/partials/layout-bottom") %>
|