diff --git a/public/sw.js b/public/sw.js index 58aa78f..10690a2 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'l484-pwa-v12' +const CACHE_NAME = 'l484-pwa-v13' const APP_SHELL = [ '/', '/manifest.webmanifest', @@ -6,15 +6,17 @@ const APP_SHELL = [ self.addEventListener('install', (event) => { event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))) + self.skipWaiting() }) self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys().then((keys) => - Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))), - ), + (async () => { + const keys = await caches.keys() + await Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) + await self.clients.claim() + })(), ) - self.clients.claim() }) self.addEventListener('fetch', (event) => { @@ -30,11 +32,13 @@ self.addEventListener('fetch', (event) => { caches.open(CACHE_NAME).then((cache) => cache.put('/', clone)) return response }) - .catch(() => caches.match('/') || caches.match(event.request)), + .catch(async () => (await caches.match('/')) || (await caches.match(event.request)) || Response.error()), ) return } + if (event.request.method !== 'GET') return + event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request).then((response) => { @@ -43,7 +47,7 @@ self.addEventListener('fetch', (event) => { caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)) } return response - }), + }).catch(() => Response.error()), ), ) }) diff --git a/server/server.js b/server/server.js index e612faa..1cfb07c 100644 --- a/server/server.js +++ b/server/server.js @@ -534,7 +534,7 @@ const applyPaidMembershipPeriod = async (payment) => { const paidThrough = addMembershipMonths(coversFrom, months) payment.months = months payment.periodLabel = months === 1 ? '1 month' : `${months} months` - payment.amountUsd = membershipMonthlyUsd * months + payment.amountUsd = payment.provider === 'comp' ? 0 : membershipMonthlyUsd * months payment.coversFrom = coversFrom.toISOString() payment.paidThrough = paidThrough.toISOString() const updated = await updateMemberRecord(payment.membershipId, (current) => ({ @@ -698,6 +698,8 @@ const updateMemberStatus = async (membershipId, status) => { return updateMemberRecord(membershipId, (member) => ({ ...member, status })) } +const serializeAdminMember = (member) => ({ ...member, accessStatus: memberAccessStatus(member) }) + const serializeAdmin = (pubkey = '') => { const allMembers = members() return { @@ -705,7 +707,7 @@ const serializeAdmin = (pubkey = '') => { admin: { isMaster: isMasterAdminPubkey(pubkey), }, - memberships: allMembers.map((member) => ({ ...member, accessStatus: memberAccessStatus(member) })), + memberships: allMembers.map(serializeAdminMember), payments: state.payments, membershipMonthlyUsd, membershipPeriodOptions, @@ -1356,13 +1358,14 @@ const handleApi = async (req, res) => { const member = findMember((item) => item.membershipId === membershipId) if (!member) return json(res, 404, { error: 'Member not found.' }) const months = normalizePaymentMonths(body.months) + const provider = cleanText(body.provider, 24) === 'comp' ? 'comp' : 'manual' const payment = { id: createId('payment'), membershipId, - provider: cleanText(body.provider, 24) || 'manual', + provider, months, periodLabel: months === 1 ? '1 month' : `${months} months`, - amountUsd: membershipMonthlyUsd * months, + amountUsd: provider === 'comp' ? 0 : membershipMonthlyUsd * months, amountSats: Number(body.amountSats || 0), status: 'paid', btcpayInvoiceId: cleanText(body.btcpayInvoiceId, 120), @@ -1370,8 +1373,12 @@ const handleApi = async (req, res) => { paidAt: new Date().toISOString(), } state.payments.unshift(payment) - await applyPaidMembershipPeriod(payment) - return json(res, 201, { success: true, payment }) + const membership = await applyPaidMembershipPeriod(payment) + return json(res, 201, { + success: true, + payment, + membership: membership ? serializeAdminMember(membership) : null, + }) } if (req.method === 'POST' && url.pathname === '/api/payment/btcpay') { diff --git a/src/App.vue b/src/App.vue index 662849e..9bb69ff 100644 --- a/src/App.vue +++ b/src/App.vue @@ -315,14 +315,16 @@ const pendingPayments = computed(() => payments.value.filter((payment) => paymen const paymentTotals = computed(() => { const paid = paidPayments.value.reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) const pending = pendingPayments.value.reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) - const cash = paidPayments.value.filter((payment) => payment.provider !== 'btcpay').reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) + const cash = paidPayments.value.filter((payment) => ['manual', 'cash'].includes(payment.provider)).reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) const bitcoin = paidPayments.value.filter((payment) => payment.provider === 'btcpay').reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) - return { paid, pending, cash, bitcoin } + const comp = paidPayments.value.filter((payment) => payment.provider === 'comp').reduce((total, payment) => total + Number(payment.amountUsd || 0), 0) + return { paid, pending, cash, bitcoin, comp } }) const paymentMethodRows = computed(() => { const rows = [ - { label: 'Cash', value: paymentTotals.value.cash, count: paidPayments.value.filter((payment) => payment.provider !== 'btcpay').length }, + { label: 'Cash', value: paymentTotals.value.cash, count: paidPayments.value.filter((payment) => ['manual', 'cash'].includes(payment.provider)).length }, { label: 'Bitcoin', value: paymentTotals.value.bitcoin, count: paidPayments.value.filter((payment) => payment.provider === 'btcpay').length }, + { label: 'Comp', value: paymentTotals.value.comp, count: paidPayments.value.filter((payment) => payment.provider === 'comp').length }, ] const max = Math.max(...rows.map((row) => row.value), 1) return rows.map((row) => ({ ...row, percentage: Math.max(4, Math.round((row.value / max) * 100)) })) @@ -1503,19 +1505,37 @@ const submitEventEnquiry = () => { eventEnquiry.message = '' } -const markManualPayment = async (membershipId, months = 1) => { +const paymentProviderLabel = (provider) => { + if (provider === 'btcpay') return 'Bitcoin' + if (provider === 'comp') return 'Comp' + return 'Cash' +} + +const markManualPayment = async (membershipId, months = 1, provider = 'manual') => { adminActionError.value = '' adminActionMessage.value = '' + const isComp = provider === 'comp' try { - await fetchJson('/api/payment/manual', { + const result = await fetchJson('/api/payment/manual', { method: 'POST', headers: adminHeaders(), - body: JSON.stringify({ membershipId, provider: 'manual', months }), + body: JSON.stringify({ membershipId, provider, months }), + }) + if (result.payment) { + payments.value = [result.payment, ...payments.value.filter((payment) => payment.id !== result.payment.id)] + } + if (result.membership) { + members.value = members.value.map((member) => + member.membershipId === result.membership.membershipId ? normalizeMember(result.membership) : member, + ) + saveMembers() + } + adminActionMessage.value = isComp ? 'Comp membership recorded.' : 'Cash payment recorded.' + await refreshAdminState().catch((error) => { + console.warn(`Could not refresh admin state after ${isComp ? 'comp' : 'cash'} payment:`, error) }) - adminActionMessage.value = 'Cash payment recorded.' - await refreshAdminState() } catch (error) { - adminActionError.value = error instanceof Error ? error.message : 'Could not record cash payment.' + adminActionError.value = error instanceof Error ? error.message : `Could not record ${isComp ? 'comp membership' : 'cash payment'}.` } } @@ -1628,6 +1648,17 @@ const takeCashPayment = async () => { if (!adminActionError.value) closePayment() } +const compMembership = async () => { + if (!selectedPaymentMember.value) return + isPaymentModalLoading.value = true + paymentModalLoadingMethod.value = 'comp' + paymentModalError.value = '' + await markManualPayment(selectedPaymentMember.value.membershipId, paymentMonths.value, 'comp') + isPaymentModalLoading.value = false + paymentModalLoadingMethod.value = '' + if (!adminActionError.value) closePayment() +} + const createBitcoinPayment = async () => { if (!selectedPaymentMember.value) return isPaymentModalLoading.value = true @@ -3235,7 +3266,7 @@ watch(mobileMenuOpen, (open) => {

Method Split

-

Cash / Bitcoin

+

Payment methods

@@ -3259,9 +3290,9 @@ watch(mobileMenuOpen, (open) => {
- {{ payment.provider === 'btcpay' ? 'Bitcoin' : 'Cash' }} + {{ paymentProviderLabel(payment.provider) }}

{{ payment.membershipId }}

- {{ formatUsd(payment.amountUsd || MEMBERSHIP_MONTHLY_USD) }} + {{ formatUsd(payment.amountUsd ?? MEMBERSHIP_MONTHLY_USD) }} {{ payment.status }} · {{ payment.periodLabel || `${payment.months || 1} month` }}
@@ -3556,6 +3587,11 @@ watch(mobileMenuOpen, (open) => { Paid in cash Record {{ paymentPeriodLabel }} paid in person. +