From dafa6ba9ebb9421653d58c583c0b8ced652e0162 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 13:28:43 -0500 Subject: [PATCH] =?UTF-8?q?Add=20ImpactReportHelper=20=E2=80=94=20annual?= =?UTF-8?q?=20impact=20summary,=20donor=20retention,=20program=20metrics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/ImpactReportHelper.php | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 source/packages/plg_system_mokosuitenpo/src/Helper/ImpactReportHelper.php diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/ImpactReportHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/ImpactReportHelper.php new file mode 100644 index 0000000..ffda5c3 --- /dev/null +++ b/source/packages/plg_system_mokosuitenpo/src/Helper/ImpactReportHelper.php @@ -0,0 +1,127 @@ +get(DatabaseInterface::class); + $from = $year . '-01-01'; + $to = $year . '-12-31'; + + // Fundraising + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS donation_count, COALESCE(SUM(amount), 0) AS total_raised, COUNT(DISTINCT donor_id) AS unique_donors') + ->from('#__mokosuitenpo_donations') + ->where('donation_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); + $fundraising = $db->loadObject(); + + // Campaigns + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS campaigns_run') + ->select('SUM(CASE WHEN COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.campaign_id = c.id), 0) >= c.goal THEN 1 ELSE 0 END) AS goals_met') + ->from($db->quoteName('#__mokosuitenpo_campaigns', 'c')) + ->where('c.start_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); + $campaigns = $db->loadObject(); + + // Volunteer hours + $db->setQuery($db->getQuery(true) + ->select('COUNT(DISTINCT volunteer_id) AS active_volunteers, COALESCE(SUM(hours), 0) AS total_hours') + ->from('#__mokosuitenpo_volunteer_hours') + ->where('date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); + $volunteers = $db->loadObject(); + + // Events + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS events_held') + ->select('(SELECT COUNT(*) FROM #__mokosuitenpo_event_registrations er JOIN #__mokosuitenpo_events e ON e.id = er.event_id WHERE e.event_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to) . ') AS total_attendees') + ->from('#__mokosuitenpo_events') + ->where('event_date BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); + $events = $db->loadObject(); + + // Grants + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS grants_received, COALESCE(SUM(amount), 0) AS grant_total') + ->from('#__mokosuitenpo_grants') + ->where($db->quoteName('status') . ' IN (' . $db->quote('awarded') . ',' . $db->quote('active') . ')') + ->where('YEAR(awarded_date) = ' . (int) $year)); + $grants = $db->loadObject(); + + // Donor retention + $priorYearDonors = 0; + $retainedDonors = 0; + try { + $db->setQuery($db->getQuery(true) + ->select('COUNT(DISTINCT donor_id)') + ->from('#__mokosuitenpo_donations') + ->where('YEAR(donation_date) = ' . ($year - 1))); + $priorYearDonors = (int) $db->loadResult(); + + $db->setQuery($db->getQuery(true) + ->select('COUNT(DISTINCT d1.donor_id)') + ->from($db->quoteName('#__mokosuitenpo_donations', 'd1')) + ->where('YEAR(d1.donation_date) = ' . (int) $year) + ->where('d1.donor_id IN (SELECT DISTINCT d2.donor_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . ($year - 1) . ')')); + $retainedDonors = (int) $db->loadResult(); + } catch (\Throwable $e) {} + + $retentionRate = $priorYearDonors > 0 ? round($retainedDonors / $priorYearDonors * 100, 1) : 0; + + return (object) [ + 'year' => $year, + 'total_raised' => (float) $fundraising->total_raised, + 'donation_count' => (int) $fundraising->donation_count, + 'unique_donors' => (int) $fundraising->unique_donors, + 'donor_retention' => $retentionRate, + 'campaigns_run' => (int) ($campaigns->campaigns_run ?? 0), + 'goals_met' => (int) ($campaigns->goals_met ?? 0), + 'volunteer_count' => (int) ($volunteers->active_volunteers ?? 0), + 'volunteer_hours' => (float) ($volunteers->total_hours ?? 0), + 'events_held' => (int) ($events->events_held ?? 0), + 'event_attendees' => (int) ($events->total_attendees ?? 0), + 'grants_received' => (int) ($grants->grants_received ?? 0), + 'grant_total' => (float) ($grants->grant_total ?? 0), + ]; + } + + /** + * Generate donor-specific impact statement. + */ + public static function getDonorImpact(int $donorId, int $year): object + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('COALESCE(SUM(amount), 0) AS given_this_year, COUNT(*) AS gift_count') + ->from('#__mokosuitenpo_donations') + ->where('donor_id = ' . (int) $donorId) + ->where('YEAR(donation_date) = ' . (int) $year)); + $giving = $db->loadObject(); + + $db->setQuery($db->getQuery(true) + ->select('lifetime_giving, first_gift_date, gift_count AS total_gifts') + ->from('#__mokosuitenpo_donors') + ->where('id = ' . (int) $donorId)); + $donor = $db->loadObject(); + + return (object) [ + 'year' => $year, + 'given_this_year' => (float) ($giving->given_this_year ?? 0), + 'gifts_this_year' => (int) ($giving->gift_count ?? 0), + 'lifetime_giving' => (float) ($donor->lifetime_giving ?? 0), + 'total_gifts' => (int) ($donor->total_gifts ?? 0), + 'member_since' => $donor->first_gift_date ?? null, + ]; + } +} -- 2.52.0