Files
MOPC-Portal/src/components/admin/pipeline/sections/notifications-section.tsx
Matt 331b67dae0 Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system.

Schema: 11 new models (Pipeline, Track, Stage, StageTransition,
ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor,
OverrideAction, AudienceVoter) + 8 new enums.

Backend: 9 new routers (pipeline, stage, routing, stageFiltering,
stageAssignment, cohort, live, decision, award) + 6 new services
(stage-engine, routing-engine, stage-filtering, stage-assignment,
stage-notifications, live-control).

Frontend: Pipeline wizard (17 components), jury stage pages (7),
applicant pipeline pages (3), public stage pages (2), admin pipeline
pages (5), shared stage components (3), SSE route, live hook.

Phase 6 refit: 23 routers/services migrated from roundId to stageId,
all frontend components refitted. Deleted round.ts (985 lines),
roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx,
10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs.

Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing,
TypeScript 0 errors, Next.js build succeeds, 13 integrity checks,
legacy symbol sweep clean, auto-seed on first Docker startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:57:09 +01:00

162 lines
5.4 KiB
TypeScript

'use client'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Card, CardContent } from '@/components/ui/card'
import { Bell } from 'lucide-react'
type NotificationsSectionProps = {
config: Record<string, boolean>
onChange: (config: Record<string, boolean>) => void
overridePolicy: Record<string, unknown>
onOverridePolicyChange: (policy: Record<string, unknown>) => void
}
const NOTIFICATION_EVENTS = [
{
key: 'stage.transitioned',
label: 'Stage Transitioned',
description: 'When a stage changes status (draft → active → closed)',
},
{
key: 'filtering.completed',
label: 'Filtering Completed',
description: 'When batch filtering finishes processing',
},
{
key: 'assignment.generated',
label: 'Assignments Generated',
description: 'When jury assignments are created or updated',
},
{
key: 'routing.executed',
label: 'Routing Executed',
description: 'When projects are routed into tracks/stages',
},
{
key: 'live.cursor.updated',
label: 'Live Cursor Updated',
description: 'When the live presentation moves to next project',
},
{
key: 'cohort.window.changed',
label: 'Cohort Window Changed',
description: 'When a cohort voting window opens or closes',
},
{
key: 'decision.overridden',
label: 'Decision Overridden',
description: 'When an admin overrides an automated decision',
},
{
key: 'award.winner.finalized',
label: 'Award Winner Finalized',
description: 'When a special award winner is selected',
},
]
export function NotificationsSection({
config,
onChange,
overridePolicy,
onOverridePolicyChange,
}: NotificationsSectionProps) {
const toggleEvent = (key: string, enabled: boolean) => {
onChange({ ...config, [key]: enabled })
}
return (
<div className="space-y-6">
<div>
<p className="text-sm text-muted-foreground">
Choose which pipeline events trigger notifications. All events are enabled by default.
</p>
</div>
<div className="space-y-2">
{NOTIFICATION_EVENTS.map((event) => (
<Card key={event.key}>
<CardContent className="py-3 px-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-start gap-3 min-w-0">
<Bell className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
<div className="min-w-0">
<Label className="text-sm font-medium">{event.label}</Label>
<p className="text-xs text-muted-foreground">{event.description}</p>
</div>
</div>
<Switch
checked={config[event.key] !== false}
onCheckedChange={(checked) => toggleEvent(event.key, checked)}
/>
</div>
</CardContent>
</Card>
))}
</div>
{/* Override Governance */}
<div className="space-y-3 pt-2 border-t">
<Label>Override Governance</Label>
<p className="text-xs text-muted-foreground">
Who can override automated decisions in this pipeline?
</p>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Switch
checked={
Array.isArray(overridePolicy.allowedRoles) &&
overridePolicy.allowedRoles.includes('SUPER_ADMIN')
}
disabled
/>
<Label className="text-sm">Super Admins (always enabled)</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={
Array.isArray(overridePolicy.allowedRoles) &&
overridePolicy.allowedRoles.includes('PROGRAM_ADMIN')
}
onCheckedChange={(checked) => {
const roles = Array.isArray(overridePolicy.allowedRoles)
? [...overridePolicy.allowedRoles]
: ['SUPER_ADMIN']
if (checked && !roles.includes('PROGRAM_ADMIN')) {
roles.push('PROGRAM_ADMIN')
} else if (!checked) {
const idx = roles.indexOf('PROGRAM_ADMIN')
if (idx >= 0) roles.splice(idx, 1)
}
onOverridePolicyChange({ ...overridePolicy, allowedRoles: roles })
}}
/>
<Label className="text-sm">Program Admins</Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={
Array.isArray(overridePolicy.allowedRoles) &&
overridePolicy.allowedRoles.includes('AWARD_MASTER')
}
onCheckedChange={(checked) => {
const roles = Array.isArray(overridePolicy.allowedRoles)
? [...overridePolicy.allowedRoles]
: ['SUPER_ADMIN']
if (checked && !roles.includes('AWARD_MASTER')) {
roles.push('AWARD_MASTER')
} else if (!checked) {
const idx = roles.indexOf('AWARD_MASTER')
if (idx >= 0) roles.splice(idx, 1)
}
onOverridePolicyChange({ ...overridePolicy, allowedRoles: roles })
}}
/>
<Label className="text-sm">Award Masters</Label>
</div>
</div>
</div>
</div>
)
}