Add user nudges for certain statuses #308

Merged
itzaname merged 2 commits from feature/status-nudge into staging 2025-12-27 23:30:38 +00:00
6 changed files with 123 additions and 6 deletions

View File

@@ -20,7 +20,12 @@ export default function AuditEventItem({ event, validatorUser }: AuditEventItemP
const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150');
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Box sx={{
display: 'flex',
gap: 2,
p: 2,
borderRadius: 1
}}>
<Box sx={{ position: 'relative', width: 40, height: 40 }}>
<Skeleton
variant="circular"

View File

@@ -4,10 +4,22 @@ import {
Box,
Tabs,
Tab,
keyframes
} from "@mui/material";
import CommentsTabPanel from './CommentsTabPanel';
import AuditEventsTabPanel from './AuditEventsTabPanel';
import { AuditEvent } from "@/app/ts/AuditEvent";
import { AuditEvent, AuditEventType } from "@/app/ts/AuditEvent";
const pulse = keyframes`
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
`;
interface CommentsAndAuditSectionProps {
auditEvents: AuditEvent[];
@@ -16,6 +28,7 @@ interface CommentsAndAuditSectionProps {
handleCommentSubmit: () => void;
validatorUser: number;
userId: number | null;
currentStatus?: number;
}
export default function CommentsAndAuditSection({
@@ -25,6 +38,7 @@ export default function CommentsAndAuditSection({
handleCommentSubmit,
validatorUser,
userId,
currentStatus,
}: CommentsAndAuditSectionProps) {
const [activeTab, setActiveTab] = useState(0);
@@ -32,6 +46,16 @@ export default function CommentsAndAuditSection({
setActiveTab(newValue);
};
// Check if there's validator feedback for changes requested status
// Show badge if status is ChangesRequested and there are validator events
const hasValidatorFeedback = currentStatus === 1 && auditEvents.some(event =>
event.User === validatorUser &&
(
event.EventType === AuditEventType.Error ||
event.EventType === AuditEventType.CheckList
)
);
return (
<Paper sx={{ p: 3, mt: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
@@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({
aria-label="comments and audit tabs"
>
<Tab label="Comments" />
<Tab label="Audit Events" />
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Audit Events
{hasValidatorFeedback && (
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: '#ff9800',
animation: `${pulse} 2s ease-in-out infinite`
}}
/>
)}
</Box>
}
/>
</Tabs>
</Box>

View File

@@ -18,11 +18,13 @@ type ReviewItemType = SubmissionInfo | MapfixInfo;
interface ReviewItemProps {
item: ReviewItemType;
handleCopyValue: (value: string) => void;
currentUserId?: number;
}
export function ReviewItem({
item,
handleCopyValue
handleCopyValue,
currentUserId
}: ReviewItemProps) {
// Type guard to check if item is valid
if (!item) return null;
@@ -105,6 +107,8 @@ export function ReviewItem({
<WorkflowStepper
currentStatus={item.StatusID}
type={isMapfix ? 'mapfix' : 'submission'}
submitterId={item.Submitter}
currentUserId={currentUserId}
/>
</Paper>
</>

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes } from '@mui/material';
import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes, Typography, Paper } from '@mui/material';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from '@mui/icons-material/Warning';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { Status } from '@/app/ts/Status';
const pulse = keyframes`
@@ -18,6 +19,8 @@ const pulse = keyframes`
interface WorkflowStepperProps {
currentStatus: number;
type: 'submission' | 'mapfix';
submitterId?: number;
currentUserId?: number;
}
// Define the workflow steps
@@ -164,19 +167,49 @@ const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChanges
);
};
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type }) => {
const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type, submitterId, currentUserId }) => {
const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow;
// Check if rejected or released
const isRejected = currentStatus === Status.Rejected;
const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing;
const isChangesRequested = currentStatus === Status.ChangesRequested;
const isUnderConstruction = currentStatus === Status.UnderConstruction;
// Find the active step
const activeStep = workflow.findIndex(step =>
step.statuses.includes(currentStatus)
);
// Determine nudge message
const getNudgeContent = () => {
if (isUnderConstruction) {
return {
icon: InfoOutlinedIcon,
title: 'Not Yet Submitted',
message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.',
color: '#2196f3',
bgColor: 'rgba(33, 150, 243, 0.08)'
};
}
if (isChangesRequested) {
return {
icon: WarningIcon,
title: 'Changes Requested',
message: 'Review comments and audit events, make modifications, and submit again.',
color: '#ff9800',
bgColor: 'rgba(255, 152, 0, 0.08)'
};
}
return null;
};
const nudge = getNudgeContent();
// Only show nudge if current user is the submitter
const isSubmitter = submitterId !== undefined && currentUserId !== undefined && submitterId === currentUserId;
const shouldShowNudge = nudge && isSubmitter;
// If rejected, show all steps as incomplete with error state
if (isRejected) {
return (
@@ -245,6 +278,36 @@ const WorkflowStepper: React.FC<WorkflowStepperProps> = ({ currentStatus, type }
);
})}
</Stepper>
{/* Action Nudge */}
{shouldShowNudge && (
<Paper
elevation={0}
sx={{
mt: 3,
p: 2,
borderRadius: 2,
borderLeft: 4,
borderColor: nudge.color,
backgroundColor: nudge.bgColor,
display: 'flex',
gap: 1.5,
alignItems: 'flex-start'
}}
>
<Box sx={{ color: nudge.color, display: 'flex', alignItems: 'center', pt: 0.25 }}>
<nudge.icon sx={{ fontSize: 24 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight={600} sx={{ color: nudge.color, mb: 0.5 }}>
{nudge.title}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
{nudge.message}
</Typography>
</Box>
</Paper>
)}
</Box>
);
};

View File

@@ -365,6 +365,7 @@ export default function MapfixDetailsPage() {
<ReviewItem
item={mapfix}
handleCopyValue={handleCopyId}
currentUserId={user ?? undefined}
/>
{/* Comments Section */}
@@ -375,6 +376,7 @@ export default function MapfixDetailsPage() {
handleCommentSubmit={handleCommentSubmit}
validatorUser={validatorUser}
userId={user}
currentStatus={mapfix.StatusID}
/>
</Grid>
</Grid>

View File

@@ -267,6 +267,7 @@ export default function SubmissionDetailsPage() {
<ReviewItem
item={submission}
handleCopyValue={handleCopyId}
currentUserId={user ?? undefined}
/>
{/* Comments Section */}
@@ -277,6 +278,7 @@ export default function SubmissionDetailsPage() {
handleCommentSubmit={handleCommentSubmit}
validatorUser={validatorUser}
userId={user}
currentStatus={submission.StatusID}
/>
</Grid>
</Grid>