From 900a0affe55576dfc5d044c2262e297b0d77f570 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Mon, 9 Mar 2026 14:15:30 -0600 Subject: [PATCH] Add CRM activity enrichment, notification bridging, crash-safe scheduled jobs, and quick wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workstream A — CRM & Notifications: - Add fire-and-forget CRM activity helper (api/src/utils/crm-activity.ts) hooked into campaign email, canvass visit, donation, and purchase write sites - Add 5 operational NotificationType enum values (shift_signup_confirmed, shift_reminder, shift_cancelled, canvass_session_summary, reengagement) via Prisma migration - Bridge notification email queue to in-app notifications for volunteer-facing events - Extend TYPE_TO_PREF map and NotificationsPage labels for new types Workstream B — Quick Wins: - Extract shared role constants (11 roles) to admin/src/utils/role-constants.ts, update 4 consuming pages - Add Ad Analytics sidebar entry in payments submenu - Gate 6 calendar routes with enableSocialCalendar feature flag - Add GET /series/:id/count endpoint and fix hardcoded shiftsCount={0} in ShiftsPage - Add influenceCampaignId to Order model for donation-campaign attribution, wire through Stripe checkout metadata Workstream C — Crash-Safe Scheduled Jobs: - Create BullMQ scheduled-jobs queue with 10 repeatable job types replacing setInterval blocks in server.ts (dynamic imports, concurrency: 2) - Keep presenceService (1min) and challengeScoringService (5min) as setInterval Bunker Admin --- admin/src/App.tsx | 16 +- admin/src/components/AppLayout.tsx | 1 + admin/src/pages/AdminCalendarPage.tsx | 17 +- admin/src/pages/AdminCalendarViewPage.tsx | 9 +- admin/src/pages/SchedulingCalendarPage.tsx | 17 +- admin/src/pages/ShiftsPage.tsx | 9 +- admin/src/pages/social/SocialGraphPage.tsx | 19 +-- .../src/pages/volunteer/NotificationsPage.tsx | 5 + admin/src/utils/role-constants.ts | 37 ++++ .../migration.sql | 16 ++ api/prisma/schema.prisma | 10 ++ .../campaign-emails.service.ts | 9 + .../modules/map/canvass/canvass.service.ts | 16 ++ .../modules/map/shifts/shift-series.routes.ts | 10 ++ .../map/shifts/shift-series.service.ts | 9 + api/src/modules/payments/donations.service.ts | 3 + .../payments/payments-public.routes.ts | 6 +- api/src/modules/payments/payments.schemas.ts | 1 + api/src/modules/payments/webhook.service.ts | 27 ++- .../modules/social/notification.service.ts | 6 + api/src/server.ts | 50 +----- .../services/notification-queue.service.ts | 40 +++++ .../services/scheduled-jobs-queue.service.ts | 160 ++++++++++++++++++ api/src/utils/crm-activity.ts | 55 ++++++ 24 files changed, 438 insertions(+), 110 deletions(-) create mode 100644 admin/src/utils/role-constants.ts create mode 100644 api/prisma/migrations/20260309100000_add_notification_types_and_donation_campaign_attribution/migration.sql create mode 100644 api/src/services/scheduled-jobs-queue.service.ts create mode 100644 api/src/utils/crm-activity.ts diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 7fc6b99b..4b5e9482 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -375,10 +375,10 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> } /> @@ -807,7 +807,9 @@ export default function App() { path="scheduling/calendar-views/:id" element={ - + + + } /> @@ -815,7 +817,9 @@ export default function App() { path="scheduling/calendar" element={ - + + + } /> diff --git a/admin/src/components/AppLayout.tsx b/admin/src/components/AppLayout.tsx index 06a58bad..37176b2e 100644 --- a/admin/src/components/AppLayout.tsx +++ b/admin/src/components/AppLayout.tsx @@ -309,6 +309,7 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use { key: '/app/payments/donation-pages', icon: , label: 'Donation Pages' }, { key: '/app/payments/donations', icon: , label: 'Donation Orders' }, { key: '/app/payments/ads', icon: , label: 'Gallery Ads' }, + { key: '/app/payments/ads/analytics', icon: , label: 'Ad Analytics' }, { key: '/app/payments/settings', icon: , label: 'Settings' }, ], }); diff --git a/admin/src/pages/AdminCalendarPage.tsx b/admin/src/pages/AdminCalendarPage.tsx index 47c4bf7f..6ef0f186 100644 --- a/admin/src/pages/AdminCalendarPage.tsx +++ b/admin/src/pages/AdminCalendarPage.tsx @@ -17,14 +17,7 @@ import dayjs from 'dayjs'; import { api } from '@/lib/api'; import type { AdminCalendarView } from '@/types/api'; import type { AppOutletContext } from '@/components/AppLayout'; - -const ROLE_OPTIONS = [ - { label: 'Super Admin', value: 'SUPER_ADMIN' }, - { label: 'Influence Admin', value: 'INFLUENCE_ADMIN' }, - { label: 'Map Admin', value: 'MAP_ADMIN' }, - { label: 'User', value: 'USER' }, - { label: 'Temp', value: 'TEMP' }, -]; +import { ROLE_COLORS, ROLE_OPTIONS } from '@/utils/role-constants'; const LAYER_TYPE_OPTIONS = [ { label: 'Shifts', value: 'SHIFTS' }, @@ -33,14 +26,6 @@ const LAYER_TYPE_OPTIONS = [ { label: 'Public Events', value: 'PUBLIC_EVENTS' }, ]; -const ROLE_COLORS: Record = { - SUPER_ADMIN: 'red', - INFLUENCE_ADMIN: 'blue', - MAP_ADMIN: 'green', - USER: 'default', - TEMP: 'orange', -}; - export default function AdminCalendarPage() { const navigate = useNavigate(); const { setPageHeader } = useOutletContext(); diff --git a/admin/src/pages/AdminCalendarViewPage.tsx b/admin/src/pages/AdminCalendarViewPage.tsx index 5d98caaf..361fca69 100644 --- a/admin/src/pages/AdminCalendarViewPage.tsx +++ b/admin/src/pages/AdminCalendarViewPage.tsx @@ -29,17 +29,10 @@ import type { AdminCalendarUser, AdminCalendarItem, } from '@/types/api'; +import { ROLE_COLORS } from '@/utils/role-constants'; const { Title, Text } = Typography; -const ROLE_COLORS: Record = { - SUPER_ADMIN: 'red', - INFLUENCE_ADMIN: 'blue', - MAP_ADMIN: 'green', - USER: 'default', - TEMP: 'orange', -}; - export default function AdminCalendarViewPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); diff --git a/admin/src/pages/SchedulingCalendarPage.tsx b/admin/src/pages/SchedulingCalendarPage.tsx index 5e128b01..23ad5506 100644 --- a/admin/src/pages/SchedulingCalendarPage.tsx +++ b/admin/src/pages/SchedulingCalendarPage.tsx @@ -24,20 +24,13 @@ import UnifiedCalendar from '@/components/calendar/UnifiedCalendar'; import { api } from '@/lib/api'; import type { UnifiedCalendarItem, AdminCalendarView } from '@/types/api'; import { useNavigate } from 'react-router-dom'; +import { ROLE_COLORS, ROLE_OPTIONS } from '@/utils/role-constants'; const { Title, Text } = Typography; const VIEWS_PANEL_WIDTH = 480; const FORM_PANEL_WIDTH = 380; -const ROLE_OPTIONS = [ - { label: 'Super Admin', value: 'SUPER_ADMIN' }, - { label: 'Influence Admin', value: 'INFLUENCE_ADMIN' }, - { label: 'Map Admin', value: 'MAP_ADMIN' }, - { label: 'User', value: 'USER' }, - { label: 'Temp', value: 'TEMP' }, -]; - const LAYER_TYPE_OPTIONS = [ { label: 'Shifts', value: 'SHIFTS' }, { label: 'Tickets', value: 'TICKETS' }, @@ -45,14 +38,6 @@ const LAYER_TYPE_OPTIONS = [ { label: 'Public Events', value: 'PUBLIC_EVENTS' }, ]; -const ROLE_COLORS: Record = { - SUPER_ADMIN: 'red', - INFLUENCE_ADMIN: 'blue', - MAP_ADMIN: 'green', - USER: 'default', - TEMP: 'orange', -}; - export default function SchedulingCalendarPage() { const navigate = useNavigate(); const addEventRef = useRef<(() => void) | null>(null); diff --git a/admin/src/pages/ShiftsPage.tsx b/admin/src/pages/ShiftsPage.tsx index c278622d..c752aa58 100644 --- a/admin/src/pages/ShiftsPage.tsx +++ b/admin/src/pages/ShiftsPage.tsx @@ -121,6 +121,7 @@ export default function ShiftsPage() { const [activeTab, setActiveTab] = useState<'table' | 'calendar'>('table'); const [editModeModalOpen, setEditModeModalOpen] = useState(false); const [editingSeriesShift, setEditingSeriesShift] = useState(null); + const [seriesShiftCount, setSeriesShiftCount] = useState(0); const [calendarData, setCalendarData] = useState({}); const [calendarLoading, setCalendarLoading] = useState(false); const [currentMonth] = useState(dayjs()); @@ -355,6 +356,12 @@ export default function ShiftsPage() { // Part of a series - show edit mode modal setEditingSeriesShift(shift); setEditModeModalOpen(true); + // Fetch series shift count + if (shift.seriesId) { + api.get(`/api/map/shifts/series/${shift.seriesId}/count`) + .then((res) => setSeriesShiftCount(res.data.count ?? 0)) + .catch(() => setSeriesShiftCount(0)); + } } else { // Regular shift or exception - edit normally openEdit(shift); @@ -1207,7 +1214,7 @@ export default function ShiftsPage() { }} onConfirm={handleEditMode} shiftDate={editingSeriesShift?.date || ''} - shiftsCount={0} // TODO: fetch series shifts count + shiftsCount={seriesShiftCount} /> ); diff --git a/admin/src/pages/social/SocialGraphPage.tsx b/admin/src/pages/social/SocialGraphPage.tsx index 0981b1b0..f850ca1c 100644 --- a/admin/src/pages/social/SocialGraphPage.tsx +++ b/admin/src/pages/social/SocialGraphPage.tsx @@ -14,25 +14,10 @@ import { ReactFlowProvider } from '@xyflow/react'; import SocialNetworkGraph, { type GraphData } from '@/components/social/SocialNetworkGraph'; import { api } from '@/lib/api'; import type { AppOutletContext } from '@/types/api'; +import { ROLE_COLORS, ROLE_FILTER_OPTIONS } from '@/utils/role-constants'; const { Text, Title } = Typography; -const ROLE_COLORS: Record = { - SUPER_ADMIN: 'red', - INFLUENCE_ADMIN: 'blue', - MAP_ADMIN: 'green', - USER: 'default', - TEMP: 'orange', -}; - -const ROLE_OPTIONS = [ - { label: 'All Roles', value: '' }, - { label: 'Super Admin', value: 'SUPER_ADMIN' }, - { label: 'Influence Admin', value: 'INFLUENCE_ADMIN' }, - { label: 'Map Admin', value: 'MAP_ADMIN' }, - { label: 'User', value: 'USER' }, -]; - type LayoutMode = 'force' | 'radial'; interface SelectedUser { @@ -145,7 +130,7 @@ function GraphPageInner() {