fix(display): cadence-aware contribution suffix across UI + admin dashboard
Add formatContribution helper in app/config/contributions.js and route all member-facing and admin contribution displays through cadence-aware expressions so annual members see /yr instead of /mo. Normalize annual amounts to monthly equivalents in the admin dashboard revenue aggregate now that contributionAmount is stored in cadence units.
This commit is contained in:
parent
5023fb14ad
commit
0dd68ff1aa
6 changed files with 29 additions and 12 deletions
|
|
@ -20,3 +20,12 @@ export const getGuidanceLabel = (amount) => {
|
||||||
const match = CONTRIBUTION_PRESETS.findLast(p => p.amount <= n)
|
const match = CONTRIBUTION_PRESETS.findLast(p => p.amount <= n)
|
||||||
return match?.label ?? null
|
return match?.label ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format a contribution amount with cadence-aware suffix.
|
||||||
|
// amount is interpreted in cadence units (e.g. 180 + 'annual' → "$180/yr").
|
||||||
|
export const formatContribution = (amount, cadence) => {
|
||||||
|
const n = Number(amount) || 0
|
||||||
|
if (n === 0) return '$0'
|
||||||
|
const suffix = cadence === 'annual' ? '/yr' : '/mo'
|
||||||
|
return `$${n}${suffix}`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Contribution ($/mo)</label>
|
<label>Contribution ({{ member.billingCadence === 'annual' ? '$/yr' : '$/mo' }})</label>
|
||||||
<input v-model.number="form.contributionAmount" type="number" min="0" step="1">
|
<input v-model.number="form.contributionAmount" type="number" min="0" step="1">
|
||||||
<p class="field-hint field-hint--warn">
|
<p class="field-hint field-hint--warn">
|
||||||
Writes to our database only. If the member is on a paid plan, also update <code>recurringAmount</code> in the Helcim dashboard — this form does not sync.
|
Writes to our database only. If the member is on a paid plan, also update <code>recurringAmount</code> in the Helcim dashboard — this form does not sync.
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
<td>
|
<td>
|
||||||
<CircleBadge :circle="member.circle" />
|
<CircleBadge :circle="member.circle" />
|
||||||
</td>
|
</td>
|
||||||
<td class="col-mono">${{ member.contributionAmount ?? 0 }}/mo</td>
|
<td class="col-mono">${{ member.contributionAmount ?? 0 }}{{ member.billingCadence === 'annual' ? '/yr' : '/mo' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge status" :class="`status-${member.status || 'pending_payment'}`">{{ statusLabel(member.status) }}</span>
|
<span class="badge status" :class="`status-${member.status || 'pending_payment'}`">{{ statusLabel(member.status) }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -192,7 +192,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Contribution ($/mo)</label>
|
<label>Contribution ({{ newMember.billingCadence === 'annual' ? '$/yr' : '$/mo' }})</label>
|
||||||
<input v-model.number="newMember.contributionAmount" type="number" min="0" step="1">
|
<input v-model.number="newMember.contributionAmount" type="number" min="0" step="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
|
|
@ -286,7 +286,7 @@
|
||||||
<td>{{ row.name }}</td>
|
<td>{{ row.name }}</td>
|
||||||
<td class="col-email">{{ row.email }}</td>
|
<td class="col-email">{{ row.email }}</td>
|
||||||
<td>{{ row.circle }}</td>
|
<td>{{ row.circle }}</td>
|
||||||
<td>${{ row.contributionAmount }}/mo</td>
|
<td>${{ row.contributionAmount }}{{ row.billingCadence === 'annual' ? '/yr' : '/mo' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -366,7 +366,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Contribution ($/mo)</label>
|
<label>Contribution ({{ editingMember.billingCadence === 'annual' ? '$/yr' : '$/mo' }})</label>
|
||||||
<input v-model.number="editingMember.contributionAmount" type="number" min="0" step="1">
|
<input v-model.number="editingMember.contributionAmount" type="number" min="0" step="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
@ -860,6 +860,7 @@ const editingMember = reactive({
|
||||||
email: "",
|
email: "",
|
||||||
circle: "community",
|
circle: "community",
|
||||||
contributionAmount: 0,
|
contributionAmount: 0,
|
||||||
|
billingCadence: "monthly",
|
||||||
status: "pending_payment",
|
status: "pending_payment",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -870,6 +871,7 @@ const editMember = (member) => {
|
||||||
email: member.email,
|
email: member.email,
|
||||||
circle: member.circle,
|
circle: member.circle,
|
||||||
contributionAmount: member.contributionAmount ?? 0,
|
contributionAmount: member.contributionAmount ?? 0,
|
||||||
|
billingCadence: member.billingCadence || "monthly",
|
||||||
status: member.status || "pending_payment",
|
status: member.status || "pending_payment",
|
||||||
});
|
});
|
||||||
showEditModal.value = true;
|
showEditModal.value = true;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<DashedBox :hoverable="false">
|
<DashedBox :hoverable="false">
|
||||||
<div class="section-label">Contribution</div>
|
<div class="section-label">Contribution</div>
|
||||||
<div class="info-value">
|
<div class="info-value">
|
||||||
${{ memberData?.contributionAmount ?? 0 }} CAD/month
|
{{ formatContribution(memberData?.contributionAmount ?? 0, memberData?.billingCadence) }} CAD
|
||||||
</div>
|
</div>
|
||||||
</DashedBox>
|
</DashedBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -308,7 +308,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
|
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { getCircleOptions } from "~/config/circles";
|
import { getCircleOptions } from "~/config/circles";
|
||||||
import { requiresPayment } from "~/config/contributions";
|
import { requiresPayment, formatContribution } from "~/config/contributions";
|
||||||
|
|
||||||
useSiteMeta({
|
useSiteMeta({
|
||||||
title: "Join",
|
title: "Join",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<PageHeader :title="welcomeTitle">
|
<PageHeader :title="welcomeTitle">
|
||||||
<div class="dashboard-meta">
|
<div class="dashboard-meta">
|
||||||
<CircleBadge :circle="memberData?.circle || 'community'" />
|
<CircleBadge :circle="memberData?.circle || 'community'" />
|
||||||
<span>${{ memberData?.contributionAmount ?? 0 }} CAD/mo</span>
|
<span>{{ formatContribution(memberData?.contributionAmount ?? 0, memberData?.billingCadence) }} CAD</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="showSlackComingNote" class="slack-coming-note">
|
<p v-if="showSlackComingNote" class="slack-coming-note">
|
||||||
Slack workspace access is part of your membership. Invitations are
|
Slack workspace access is part of your membership. Invitations are
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
<div class="membership-row">
|
<div class="membership-row">
|
||||||
<span class="key">Contribution</span>
|
<span class="key">Contribution</span>
|
||||||
<span class="val"
|
<span class="val"
|
||||||
>${{ memberData?.contributionAmount ?? 0 }} CAD/month</span
|
>{{ formatContribution(memberData?.contributionAmount ?? 0, memberData?.billingCadence) }} CAD</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="membership-row">
|
<div class="membership-row">
|
||||||
|
|
@ -220,6 +220,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { formatContribution } from '~/config/contributions';
|
||||||
|
|
||||||
useSiteMeta({ title: 'Dashboard', noindex: true });
|
useSiteMeta({ title: 'Dashboard', noindex: true });
|
||||||
|
|
||||||
const { memberData, checkMemberStatus } = useAuth();
|
const { memberData, checkMemberStatus } = useAuth();
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,14 @@ export default defineEventHandler(async (event) => {
|
||||||
endDate: { $gte: now }
|
endDate: { $gte: now }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Calculate monthly revenue from member contributions
|
// Calculate monthly revenue from member contributions.
|
||||||
const members = await Member.find({}, 'contributionAmount').lean()
|
// contributionAmount is stored in cadence units (monthly = $/mo, annual = $/yr),
|
||||||
|
// so normalize annual amounts to monthly equivalents before summing.
|
||||||
|
const members = await Member.find({}, 'contributionAmount billingCadence').lean()
|
||||||
const monthlyRevenue = members.reduce((total, member) => {
|
const monthlyRevenue = members.reduce((total, member) => {
|
||||||
return total + (member.contributionAmount || 0)
|
const amt = member.contributionAmount || 0
|
||||||
|
const monthlyEquivalent = member.billingCadence === 'annual' ? amt / 12 : amt
|
||||||
|
return total + monthlyEquivalent
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
const pendingSlackInvites = await Member.countDocuments({ slackInvited: false })
|
const pendingSlackInvites = await Member.countDocuments({ slackInvited: false })
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue