diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 79f6dd3..75a6963 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
-# VERSION: 01.06.00
+# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/source/packages/com_mokosuitenpo/mokosuitenpo.xml b/source/packages/com_mokosuitenpo/mokosuitenpo.xml
index 6bc861e..5300382 100644
--- a/source/packages/com_mokosuitenpo/mokosuitenpo.xml
+++ b/source/packages/com_mokosuitenpo/mokosuitenpo.xml
@@ -7,7 +7,7 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 01.06.00
+ 01.06.02
8.3
MokoSuite NPO component
Moko\Component\MokoSuiteNpo
diff --git a/source/packages/plg_system_mokosuitenpo/src/Helper/BoardManagementHelper.php b/source/packages/plg_system_mokosuitenpo/src/Helper/BoardManagementHelper.php
new file mode 100644
index 0000000..a47ef9d
--- /dev/null
+++ b/source/packages/plg_system_mokosuitenpo/src/Helper/BoardManagementHelper.php
@@ -0,0 +1,105 @@
+get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('bm.*, cd.name, cd.email_to, cd.telephone')
+ ->select('CASE WHEN bm.term_end < NOW() THEN ' . $db->quote('expired')
+ . ' WHEN bm.term_end < DATE_ADD(NOW(), INTERVAL 90 DAY) THEN ' . $db->quote('expiring_soon')
+ . ' ELSE ' . $db->quote('active') . ' END AS term_status')
+ ->from($db->quoteName('#__mokosuitenpo_board_members', 'bm'))
+ ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = bm.contact_id')
+ ->where($db->quoteName('bm.status') . ' = ' . $db->quote('active'))
+ ->order('bm.role ASC, cd.name ASC'));
+
+ return $db->loadObjectList() ?: [];
+ }
+
+ /**
+ * Get committee assignments.
+ */
+ public static function getCommittees(): array
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $db->setQuery($db->getQuery(true)
+ ->select('c.id, c.name AS committee_name, c.description')
+ ->select('(SELECT COUNT(*) FROM #__mokosuitenpo_committee_members cm WHERE cm.committee_id = c.id AND cm.status = ' . $db->quote('active') . ') AS member_count')
+ ->select('(SELECT cd2.name FROM #__mokosuitenpo_committee_members cm2'
+ . ' JOIN #__contact_details cd2 ON cd2.id = cm2.contact_id'
+ . ' WHERE cm2.committee_id = c.id AND cm2.role = ' . $db->quote('chair')
+ . ' AND cm2.status = ' . $db->quote('active') . ' LIMIT 1) AS chair_name')
+ ->from($db->quoteName('#__mokosuitenpo_committees', 'c'))
+ ->where($db->quoteName('c.status') . ' = ' . $db->quote('active'))
+ ->order('c.name ASC'));
+
+ return $db->loadObjectList() ?: [];
+ }
+
+ /**
+ * Get meeting attendance rate for a board member.
+ */
+ public static function getAttendanceRate(int $contactId, int $months = 12): object
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $since = date('Y-m-d', strtotime("-{$months} months"));
+
+ $db->setQuery($db->getQuery(true)
+ ->select('COUNT(*) AS total_meetings')
+ ->select('SUM(CASE WHEN ma.status = ' . $db->quote('present') . ' THEN 1 ELSE 0 END) AS attended')
+ ->select('SUM(CASE WHEN ma.status = ' . $db->quote('absent') . ' THEN 1 ELSE 0 END) AS absent')
+ ->select('SUM(CASE WHEN ma.status = ' . $db->quote('excused') . ' THEN 1 ELSE 0 END) AS excused')
+ ->from($db->quoteName('#__mokosuitenpo_meeting_attendance', 'ma'))
+ ->join('INNER', $db->quoteName('#__mokosuitenpo_meetings', 'm') . ' ON m.id = ma.meeting_id')
+ ->where('ma.contact_id = ' . (int) $contactId)
+ ->where('m.meeting_date >= ' . $db->quote($since)));
+
+ $stats = $db->loadObject();
+ $total = (int) ($stats->total_meetings ?? 0);
+
+ return (object) [
+ 'contact_id' => $contactId,
+ 'total_meetings' => $total,
+ 'attended' => (int) ($stats->attended ?? 0),
+ 'absent' => (int) ($stats->absent ?? 0),
+ 'excused' => (int) ($stats->excused ?? 0),
+ 'attendance_pct' => $total > 0 ? round((int) $stats->attended / $total * 100, 1) : 0,
+ ];
+ }
+
+ /**
+ * Get terms expiring within N days.
+ */
+ public static function getExpiringTerms(int $days = 90): array
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $cutoff = date('Y-m-d', strtotime("+{$days} days"));
+
+ $db->setQuery($db->getQuery(true)
+ ->select('bm.id, bm.role, bm.term_start, bm.term_end')
+ ->select('cd.name, cd.email_to')
+ ->from($db->quoteName('#__mokosuitenpo_board_members', 'bm'))
+ ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = bm.contact_id')
+ ->where($db->quoteName('bm.status') . ' = ' . $db->quote('active'))
+ ->where('bm.term_end BETWEEN ' . $db->quote(date('Y-m-d')) . ' AND ' . $db->quote($cutoff))
+ ->order('bm.term_end ASC'));
+
+ return $db->loadObjectList() ?: [];
+ }
+}
diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml
index f6fc2bf..add91bc 100644
--- a/source/pkg_mokosuitenpo.xml
+++ b/source/pkg_mokosuitenpo.xml
@@ -2,7 +2,7 @@
Package - MokoSuite NPO
mokosuitenpo
- 01.06.00
+ 01.06.02
2026-06-11
Moko Consulting
hello@mokoconsulting.tech