Other files

This commit is contained in:
2026-04-10 18:30:04 -04:00
parent ea4ddd7d41
commit bcbfe2797c
17 changed files with 2221 additions and 16 deletions
+193 -7
View File
@@ -1219,10 +1219,10 @@ async function loadUsers() {
const data = await api('/users');
if (!data) return;
renderUsersList(data.users || []);
await renderUsersList(data.users || []);
}
function renderUsersList(users) {
async function renderUsersList(users) {
const container = document.getElementById('usersList');
if (!container) return;
@@ -1231,24 +1231,210 @@ function renderUsersList(users) {
return;
}
container.innerHTML = users.map(user => `
// Fetch profile status for all users in parallel
const profileStatuses = await Promise.all(
users.map(u => api(`/users/${u.id}/profile`).catch(() => null))
);
container.innerHTML = users.map((user, i) => {
const ps = profileStatuses[i] || {};
const hasProfile = ps.has_profile;
const isUnlocked = ps.is_unlocked;
const maskedCard = ps.masked_card || '';
const profileBadge = isUnlocked
? `<span class="badge badge-success">&#128275; Unlocked ${maskedCard ? '· ' + escapeHtml(maskedCard) : ''}</span>`
: hasProfile
? `<span class="badge badge-warning">&#128274; Locked</span>`
: `<span class="badge badge-muted">No profile</span>`;
const profileBtns = hasProfile
? isUnlocked
? `<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
<button class="btn-secondary btn-small" onclick="lockProfile(${user.id})">Lock</button>
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
: `<button class="btn-primary btn-small" onclick="openUnlockModal(${user.id}, '${escapeHtml(user.name)}')">Unlock</button>
<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
: `<button class="btn-primary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Set Up Profile</button>`;
return `
<div class="user-card" data-user-id="${user.id}">
<div class="user-header">
<span class="user-name">${escapeHtml(user.name)}</span>
<span class="user-location">${user.zip_code ? `${user.zip_code} (${user.radius_miles}mi)` : 'No location set'}</span>
${profileBadge}
</div>
<div class="user-details">
<div class="form-inline">
<input type="text" class="user-zip-input" placeholder="Zip Code" value="${user.zip_code || ''}" maxlength="10">
<input type="number" class="user-radius-input" placeholder="Radius" value="${user.radius_miles || 25}" min="5" max="100" style="width: 80px;">
<button class="btn-secondary btn-small" onclick="updateUserLocation(${user.id})">Update</button>
<button class="btn-secondary btn-small" onclick="updateUserLocation(${user.id})">Update Location</button>
<button class="btn-secondary btn-small" onclick="viewUserStores(${user.id})">View Stores</button>
<button class="btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
<button class="btn-danger btn-small" onclick="deleteUser(${user.id})">Delete User</button>
</div>
<div class="form-inline" style="margin-top:.5rem;">
${profileBtns}
</div>
</div>
<div class="user-stores" id="userStores-${user.id}" style="display: none;"></div>
</div>
`).join('');
</div>`;
}).join('');
}
// ---- Profile modal ----
let _profileUserId = null;
async function openProfileModal(userId, userName) {
_profileUserId = userId;
document.getElementById('profileModalTitle').textContent = `Checkout Profile — ${userName}`;
document.getElementById('profileModalError').style.display = 'none';
// Clear all fields first
['pf-first-name','pf-last-name','pf-address1','pf-address2','pf-city','pf-state',
'pf-zip','pf-phone','pf-email','pf-card-number','pf-expiry-month','pf-expiry-year',
'pf-cvv','pf-card-name','pf-target-email','pf-target-password',
'pf-bestbuy-email','pf-bestbuy-password','pf-gamestop-email','pf-gamestop-password',
'pf-password','pf-password-confirm'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
// Pre-fill from unlocked profile if available
const data = await api(`/users/${userId}/profile/data`).catch(() => null);
if (data && data.shipping) {
const s = data.shipping;
const set = (id, val) => { const el = document.getElementById(id); if (el && val) el.value = val; };
set('pf-first-name', s.first_name);
set('pf-last-name', s.last_name);
set('pf-address1', s.address1);
set('pf-address2', s.address2);
set('pf-city', s.city);
set('pf-state', s.state);
set('pf-zip', s.zip);
set('pf-phone', s.phone);
set('pf-email', s.email);
set('pf-target-email', data.site_credentials?.target_email);
set('pf-bestbuy-email', data.site_credentials?.bestbuy_email);
set('pf-gamestop-email', data.site_credentials?.gamestop_email);
// Show masked card as placeholder so user knows card is saved
if (data.masked_card) {
const cardEl = document.getElementById('pf-card-number');
if (cardEl) cardEl.placeholder = `${data.masked_card} (leave blank to keep)`;
}
}
document.getElementById('profileModal').style.display = 'flex';
}
function closeProfileModal() {
document.getElementById('profileModal').style.display = 'none';
_profileUserId = null;
}
async function saveProfile() {
const errEl = document.getElementById('profileModalError');
errEl.style.display = 'none';
const password = document.getElementById('pf-password').value;
const confirm = document.getElementById('pf-password-confirm').value;
if (password.length < 8) {
errEl.textContent = 'Password must be at least 8 characters.';
errEl.style.display = 'block';
return;
}
if (password !== confirm) {
errEl.textContent = 'Passwords do not match.';
errEl.style.display = 'block';
return;
}
const shipping = {
first_name: document.getElementById('pf-first-name').value.trim(),
last_name: document.getElementById('pf-last-name').value.trim(),
address1: document.getElementById('pf-address1').value.trim(),
address2: document.getElementById('pf-address2').value.trim(),
city: document.getElementById('pf-city').value.trim(),
state: document.getElementById('pf-state').value.trim().toUpperCase(),
zip: document.getElementById('pf-zip').value.trim(),
phone: document.getElementById('pf-phone').value.trim(),
email: document.getElementById('pf-email').value.trim(),
};
const payment = {
card_number: document.getElementById('pf-card-number').value.replace(/\s/g, ''),
expiry_month: document.getElementById('pf-expiry-month').value.trim(),
expiry_year: document.getElementById('pf-expiry-year').value.trim(),
cvv: document.getElementById('pf-cvv').value.trim(),
name_on_card: document.getElementById('pf-card-name').value.trim(),
};
const site_credentials = {
target_email: document.getElementById('pf-target-email')?.value.trim(),
target_password: document.getElementById('pf-target-password')?.value,
bestbuy_email: document.getElementById('pf-bestbuy-email')?.value.trim(),
bestbuy_password: document.getElementById('pf-bestbuy-password')?.value,
gamestop_email: document.getElementById('pf-gamestop-email')?.value.trim(),
gamestop_password: document.getElementById('pf-gamestop-password')?.value,
};
const result = await api(`/users/${_profileUserId}/profile`, {
method: 'POST',
body: JSON.stringify({ password, shipping, payment, site_credentials })
});
if (result && result.success) {
closeProfileModal();
loadUsers();
} else {
errEl.textContent = result?.error || 'Failed to save profile.';
errEl.style.display = 'block';
}
}
// ---- Unlock modal ----
let _unlockUserId = null;
function openUnlockModal(userId, userName) {
_unlockUserId = userId;
document.getElementById('unlockModalTitle').textContent = `Unlock Profile — ${userName}`;
document.getElementById('unlock-password').value = '';
document.getElementById('unlockModalError').style.display = 'none';
document.getElementById('unlockModal').style.display = 'flex';
setTimeout(() => document.getElementById('unlock-password').focus(), 100);
}
function closeUnlockModal() {
document.getElementById('unlockModal').style.display = 'none';
_unlockUserId = null;
}
async function submitUnlock() {
const errEl = document.getElementById('unlockModalError');
const password = document.getElementById('unlock-password').value;
const result = await api(`/users/${_unlockUserId}/profile/unlock`, {
method: 'POST',
body: JSON.stringify({ password })
});
if (result && result.success) {
closeUnlockModal();
loadUsers();
} else {
errEl.textContent = result?.error || 'Incorrect password.';
errEl.style.display = 'block';
}
}
async function lockProfile(userId) {
await api(`/users/${userId}/profile/lock`, { method: 'POST' });
loadUsers();
}
async function deleteProfile(userId) {
if (!confirm('Delete this checkout profile? This cannot be undone.')) return;
await api(`/users/${userId}/profile`, { method: 'DELETE' });
loadUsers();
}
async function addUser() {
+68 -6
View File
@@ -922,6 +922,9 @@ h3 {
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.modal.show {
@@ -931,25 +934,84 @@ h3 {
.modal-content {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
max-width: 520px;
width: 100%;
position: relative;
display: flex;
flex-direction: column;
max-height: 90vh;
margin: auto;
}
.modal-lg {
max-width: 680px;
}
.modal-header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.modal-header h3 {
margin: 0;
padding-right: 32px;
}
.modal-body {
padding: 20px 24px;
overflow-y: auto;
flex: 1;
}
.modal-body h4 {
margin: 1.2rem 0 0.6rem;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.modal-body h4:first-child {
margin-top: 0;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 10px;
flex-shrink: 0;
}
.modal-close {
position: absolute;
top: 15px;
right: 20px;
font-size: 24px;
top: 16px;
right: 18px;
font-size: 22px;
cursor: pointer;
color: var(--text-secondary);
line-height: 1;
background: none;
border: none;
}
.modal-close:hover {
color: var(--text-primary);
}
.form-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.form-row .form-group {
flex: 1;
min-width: 120px;
}
/* Loading */
.loading {
text-align: center;