feat: conditional & match funding pledges — deeply integrated across entire product

- Schema: isConditional, conditionType, conditionText, conditionThreshold, conditionMet, conditionMetAt on Pledge
- Pledge form: 'This is a match pledge' toggle after amount selection
  - Two modes: threshold (if target is reached) and match (match funding)
  - Goal amount passed through from event
- Auto-trigger: when total raised hits threshold, conditional pledges unlock automatically
  - WhatsApp notification sent to donor when unlocked
  - Threshold check runs after every pledge creation AND every status change
- Cron: skips conditional pledges until conditionMet=true (no premature reminders)
- Dashboard Home: progress bar shows conditional segment (amber), stats grid adds Conditional column
- Dashboard Money: conditional/unlocked badge on pledge rows
- Dashboard Collect: hero shows conditional total in amber
- Dashboard Reports: financial summary shows conditional breakdown
- Donor 'My Pledges': conditional card with condition text + activation status
- Confirmation step: specialized messaging for match pledges
- CRM export: includes is_conditional, condition_type, condition_text, condition_met columns
- Status guide: conditional status explained in human language
This commit is contained in:
2026-03-05 04:19:23 +08:00
parent c11bf4bea7
commit 50d449e2b7
23 changed files with 607 additions and 140 deletions

View File

@@ -211,6 +211,8 @@ export default function CollectPage() {
// Stats
const totalPledges = events.reduce((s, e) => s + e.pledgeCount, 0)
const totalPledged = events.reduce((s, e) => s + e.totalPledged, 0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const totalConditional = events.reduce((s, e) => s + ((e as any).totalConditional || 0), 0)
// Sort sources by pledges
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sortedSources = [...sources].sort((a: any, b: any) => b.totalPledged - a.totalPledged)
@@ -548,8 +550,14 @@ export default function CollectPage() {
</div>
<div>
<p className="text-2xl md:text-3xl font-black text-[#4ADE80] tracking-tight">{formatPence(totalPledged)}</p>
<p className="text-[10px] text-gray-500 mt-0.5">raised</p>
<p className="text-[10px] text-gray-500 mt-0.5">confirmed</p>
</div>
{totalConditional > 0 && (
<div>
<p className="text-2xl md:text-3xl font-black text-[#FBBF24] tracking-tight">{formatPence(totalConditional)}</p>
<p className="text-[10px] text-gray-500 mt-0.5">conditional</p>
</div>
)}
</div>
</div>
</div>