tractatus/public/js/koha-donation.js
TheFlow 44a91e7fcf feat: add case submission portal admin interface and i18n support
Case Submission Portal (Admin Moderation Queue):
- Add statistics endpoint (GET /api/cases/submissions/stats)
- Enhance filtering: status, failure_mode, AI relevance score
- Add sorting options: date, relevance, completeness
- Create admin moderation interface (case-moderation.html)
- Implement CSP-compliant admin UI (no inline event handlers)
- Deploy moderation actions: approve, reject, request-info
- Fix API parameter mapping for different action types

Internationalization (i18n):
- Implement lightweight i18n system (i18n-simple.js, ~5KB)
- Add language selector component with flag emojis
- Create German and French translations for homepage
- Document Te Reo Māori translation requirements
- Add i18n attributes to homepage
- Integrate language selector into navbar

Bug Fixes:
- Fix search button modal display on docs.html (remove conflicting flex class)

Page Enhancements:
- Add dedicated JS modules for researcher, leader, koha pages
- Improve page-specific functionality and interactions

Documentation:
- Add I18N_IMPLEMENTATION_SUMMARY.md (implementation guide)
- Add TE_REO_MAORI_TRANSLATION_REQUIREMENTS.md (cultural sensitivity guide)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 14:50:47 +13:00

289 lines
9.1 KiB
JavaScript

/**
* Koha Donation System
* Handles donation form functionality with CSP compliance
*/
// Form state
let selectedFrequency = 'monthly';
let selectedTier = '15';
let selectedAmount = 1500; // in cents (NZD)
let currentCurrency = typeof detectUserCurrency === 'function' ? detectUserCurrency() : 'NZD';
document.addEventListener('DOMContentLoaded', function() {
// Initialize event listeners
initializeFrequencyButtons();
initializeTierCards();
initializePublicAcknowledgement();
initializeDonationForm();
});
/**
* Initialize frequency selection buttons
*/
function initializeFrequencyButtons() {
const monthlyBtn = document.getElementById('freq-monthly');
const onetimeBtn = document.getElementById('freq-onetime');
if (monthlyBtn) {
monthlyBtn.addEventListener('click', function() {
selectFrequency('monthly');
});
}
if (onetimeBtn) {
onetimeBtn.addEventListener('click', function() {
selectFrequency('one_time');
});
}
}
/**
* Initialize tier card click handlers
*/
function initializeTierCards() {
const tierCards = document.querySelectorAll('[data-tier]');
tierCards.forEach(card => {
card.addEventListener('click', function() {
const tier = this.dataset.tier;
const amount = parseInt(this.dataset.amount);
selectTier(tier, amount);
});
});
}
/**
* Initialize public acknowledgement checkbox
*/
function initializePublicAcknowledgement() {
const checkbox = document.getElementById('public-acknowledgement');
if (checkbox) {
checkbox.addEventListener('change', togglePublicName);
}
}
/**
* Initialize donation form submission
*/
function initializeDonationForm() {
const form = document.getElementById('donation-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}
}
/**
* Update prices when currency changes
*/
window.updatePricesForCurrency = function(currency) {
currentCurrency = currency;
if (typeof getTierPrices !== 'function' || typeof formatCurrency !== 'function') {
console.warn('Currency utilities not loaded');
return;
}
const prices = getTierPrices(currency);
// Update tier card prices
const tierCards = document.querySelectorAll('.tier-card');
if (tierCards[0]) {
const priceEl = tierCards[0].querySelector('.text-4xl');
const currencyEl = tierCards[0].querySelector('.text-sm.text-gray-500');
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_5, currency).replace(/\.\d+$/, '');
if (currencyEl) currencyEl.textContent = `${currency} / month`;
}
if (tierCards[1]) {
const priceEl = tierCards[1].querySelector('.text-4xl');
const currencyEl = tierCards[1].querySelector('.text-sm.text-gray-500');
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_15, currency).replace(/\.\d+$/, '');
if (currencyEl) currencyEl.textContent = `${currency} / month`;
}
if (tierCards[2]) {
const priceEl = tierCards[2].querySelector('.text-4xl');
const currencyEl = tierCards[2].querySelector('.text-sm.text-gray-500');
if (priceEl) priceEl.textContent = formatCurrency(prices.tier_50, currency).replace(/\.\d+$/, '');
if (currencyEl) currencyEl.textContent = `${currency} / month`;
}
// Update custom amount placeholder
const amountInput = document.getElementById('amount-input');
if (amountInput) {
amountInput.placeholder = 'Enter amount';
const amountCurrencyLabel = amountInput.nextElementSibling;
if (amountCurrencyLabel) {
amountCurrencyLabel.textContent = currency;
}
}
// Update help text
const amountHelp = document.getElementById('amount-help');
if (amountHelp && typeof formatCurrency === 'function') {
amountHelp.textContent = `Minimum donation: ${formatCurrency(100, currency)}`;
}
};
/**
* Select donation frequency (monthly or one-time)
*/
function selectFrequency(freq) {
selectedFrequency = freq;
// Update button styles
const monthlyBtn = document.getElementById('freq-monthly');
const onetimeBtn = document.getElementById('freq-onetime');
if (freq === 'monthly') {
monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition';
onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition';
// Show tier selection, hide custom amount
const tierSelection = document.getElementById('tier-selection');
const customAmount = document.getElementById('custom-amount');
if (tierSelection) tierSelection.classList.remove('hidden');
if (customAmount) customAmount.classList.add('hidden');
} else {
monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition';
onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition';
// Hide tier selection, show custom amount
const tierSelection = document.getElementById('tier-selection');
const customAmount = document.getElementById('custom-amount');
if (tierSelection) tierSelection.classList.add('hidden');
if (customAmount) customAmount.classList.remove('hidden');
const amountInput = document.getElementById('amount-input');
if (amountInput) amountInput.focus();
}
}
/**
* Select tier amount
*/
function selectTier(tier, amountNZDCents) {
selectedTier = tier;
// Calculate amount in current currency
if (typeof getTierPrices === 'function') {
const prices = getTierPrices(currentCurrency);
selectedAmount = prices[`tier_${tier}`];
} else {
selectedAmount = amountNZDCents;
}
// Update card styles
const cards = document.querySelectorAll('.tier-card');
cards.forEach(card => {
card.classList.remove('selected');
});
// Add selected class to clicked card
const selectedCard = document.querySelector(`[data-tier="${tier}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
}
/**
* Toggle public name field visibility
*/
function togglePublicName() {
const checkbox = document.getElementById('public-acknowledgement');
const nameField = document.getElementById('public-name-field');
if (checkbox && nameField) {
if (checkbox.checked) {
nameField.classList.remove('hidden');
const publicNameInput = document.getElementById('public-name');
if (publicNameInput) publicNameInput.focus();
} else {
nameField.classList.add('hidden');
}
}
}
/**
* Handle form submission
*/
async function handleFormSubmit(e) {
e.preventDefault();
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
try {
// Get form data
const donorName = document.getElementById('donor-name').value.trim() || 'Anonymous';
const donorEmail = document.getElementById('donor-email').value.trim();
const donorCountry = document.getElementById('donor-country').value.trim();
const publicAck = document.getElementById('public-acknowledgement').checked;
const publicName = document.getElementById('public-name').value.trim();
// Validate email
if (!donorEmail) {
alert('Please enter your email address.');
submitBtn.disabled = false;
submitBtn.textContent = 'Proceed to Secure Payment';
return;
}
// Get amount
let amount;
if (selectedFrequency === 'monthly') {
amount = selectedAmount;
} else {
const customAmount = parseFloat(document.getElementById('amount-input').value);
if (!customAmount || customAmount < 1) {
const minAmount = typeof formatCurrency === 'function'
? formatCurrency(100, currentCurrency)
: `${currentCurrency} 1.00`;
alert(`Please enter a donation amount of at least ${minAmount}.`);
submitBtn.disabled = false;
submitBtn.textContent = 'Proceed to Secure Payment';
return;
}
amount = Math.round(customAmount * 100); // Convert to cents
}
// Create checkout session
const response = await fetch('/api/koha/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: amount,
currency: currentCurrency,
frequency: selectedFrequency,
tier: selectedFrequency === 'monthly' ? selectedTier : 'custom',
donor: {
name: donorName,
email: donorEmail,
country: donorCountry
},
public_acknowledgement: publicAck,
public_name: publicAck ? (publicName || donorName) : null
})
});
const data = await response.json();
if (data.success && data.data.checkoutUrl) {
// Redirect to Stripe Checkout
window.location.href = data.data.checkoutUrl;
} else {
throw new Error(data.error || 'Failed to create checkout session');
}
} catch (error) {
console.error('Donation error:', error);
alert('An error occurred while processing your donation. Please try again or contact support.');
submitBtn.disabled = false;
submitBtn.textContent = 'Proceed to Secure Payment';
}
}