Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -392,11 +392,14 @@ export default function ProjectsPage() {
const [allMatchingSelected, setAllMatchingSelected] = useState(false)
const [bulkStatus, setBulkStatus] = useState<string>('')
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false)
const [bulkNotificationsConfirmed, setBulkNotificationsConfirmed] = useState(false)
const [bulkAction, setBulkAction] = useState<'status' | 'assign' | 'delete'>('status')
const [bulkAssignStageId, setBulkAssignStageId] = useState('')
const [bulkAssignDialogOpen, setBulkAssignDialogOpen] = useState(false)
const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false)
const bulkStatusTriggersNotifications = ['SEMIFINALIST', 'FINALIST', 'REJECTED'].includes(bulkStatus)
// Query for fetching all matching IDs (used for "select all across pages")
const allIdsQuery = trpc.project.listAllIds.useQuery(
{
@@ -452,6 +455,26 @@ export default function ProjectsPage() {
},
})
const bulkNotificationPreview = trpc.project.previewStatusNotificationRecipients.useQuery(
{
ids: Array.from(selectedIds),
status: (bulkStatus || 'SUBMITTED') as
| 'SUBMITTED'
| 'ELIGIBLE'
| 'ASSIGNED'
| 'SEMIFINALIST'
| 'FINALIST'
| 'REJECTED',
},
{
enabled:
bulkConfirmOpen &&
selectedIds.size > 0 &&
bulkStatusTriggersNotifications,
staleTime: 30_000,
}
)
const bulkAssignToStage = trpc.projectPool.assignToStage.useMutation({
onSuccess: (result) => {
toast.success(`${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} assigned to stage`)
@@ -524,15 +547,21 @@ export default function ProjectsPage() {
setSelectedIds(new Set())
setAllMatchingSelected(false)
setBulkStatus('')
setBulkNotificationsConfirmed(false)
}
const handleBulkApply = () => {
if (!bulkStatus || selectedIds.size === 0) return
setBulkNotificationsConfirmed(false)
setBulkConfirmOpen(true)
}
const handleBulkConfirm = () => {
if (!bulkStatus || selectedIds.size === 0) return
if (bulkStatusTriggersNotifications && !bulkNotificationsConfirmed) {
toast.error('Confirm participant recipients before sending notifications')
return
}
bulkUpdateStatus.mutate({
ids: Array.from(selectedIds),
status: bulkStatus as 'SUBMITTED' | 'ELIGIBLE' | 'ASSIGNED' | 'SEMIFINALIST' | 'FINALIST' | 'REJECTED',
@@ -1283,7 +1312,15 @@ export default function ProjectsPage() {
)}
{/* Bulk Status Update Confirmation Dialog */}
<AlertDialog open={bulkConfirmOpen} onOpenChange={setBulkConfirmOpen}>
<AlertDialog
open={bulkConfirmOpen}
onOpenChange={(open) => {
setBulkConfirmOpen(open)
if (!open) {
setBulkNotificationsConfirmed(false)
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Update Project Status</AlertDialogTitle>
@@ -1302,6 +1339,64 @@ export default function ProjectsPage() {
</p>
</div>
)}
{bulkStatusTriggersNotifications && (
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
<p className="text-sm font-medium">Participant Notification Check</p>
<p className="text-xs text-muted-foreground">
Review recipients before automated emails are sent.
</p>
{bulkNotificationPreview.isLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Loading recipients...
</div>
) : bulkNotificationPreview.data ? (
<div className="space-y-2">
<p className="text-xs text-muted-foreground">
{bulkNotificationPreview.data.totalRecipients} recipient
{bulkNotificationPreview.data.totalRecipients !== 1 ? 's' : ''} across{' '}
{bulkNotificationPreview.data.projectsWithRecipients} project
{bulkNotificationPreview.data.projectsWithRecipients !== 1 ? 's' : ''}.
</p>
<div className="max-h-44 space-y-2 overflow-auto rounded-md border bg-background p-2">
{bulkNotificationPreview.data.projects
.filter((project) => project.recipientCount > 0)
.slice(0, 8)
.map((project) => (
<div key={project.id} className="text-xs">
<p className="font-medium">
{project.title} ({project.recipientCount})
</p>
<p className="text-muted-foreground">
{project.recipientsPreview.join(', ')}
{project.hasMoreRecipients ? ', ...' : ''}
</p>
</div>
))}
{bulkNotificationPreview.data.projectsWithRecipients === 0 && (
<p className="text-xs text-amber-700">
No linked participant accounts found. Status will update, but no team notifications will be sent.
</p>
)}
</div>
</div>
) : null}
<div className="flex items-start gap-2">
<Checkbox
id="bulk-notification-confirm"
checked={bulkNotificationsConfirmed}
onCheckedChange={(checked) => setBulkNotificationsConfirmed(checked === true)}
/>
<Label htmlFor="bulk-notification-confirm" className="text-sm font-normal leading-5">
I verified the recipient list and want to send these automated notifications.
</Label>
</div>
</div>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
@@ -1310,12 +1405,12 @@ export default function ProjectsPage() {
<AlertDialogAction
onClick={handleBulkConfirm}
className={bulkStatus === 'REJECTED' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}
disabled={bulkUpdateStatus.isPending}
disabled={bulkUpdateStatus.isPending || (bulkStatusTriggersNotifications && !bulkNotificationsConfirmed)}
>
{bulkUpdateStatus.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : null}
Update {selectedIds.size} Project{selectedIds.size !== 1 ? 's' : ''}
{bulkStatusTriggersNotifications ? 'Update + Notify' : 'Update'} {selectedIds.size} Project{selectedIds.size !== 1 ? 's' : ''}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>