From 9c7cea23a72a9e182228a512de6b8ef3240f06e8 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 14:24:43 -0400 Subject: [PATCH] Add More options to AddTaskModal to handle most of the things we have in editChore --- src/views/components/AddTaskModal.jsx | 83 +++- .../components/AdvancedOptionsSection.jsx | 361 ++++++++++++++++++ 2 files changed, 430 insertions(+), 14 deletions(-) create mode 100644 src/views/components/AdvancedOptionsSection.jsx diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 92878d1..b6f2a7e 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -25,6 +25,9 @@ import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' import { useDocumentScanner } from '../../hooks/useDocumentScanner' import { localAIService } from '../../service/LocalAIService' import { TASK_COLOR } from '../../utils/Colors' +import AdvancedOptionsSection, { + AdvancedOptionsTrigger, +} from './AdvancedOptionsSection' import AssigneePickerField from './AssigneePickerField' import AttachmentPickerField from './AttachmentPickerField' import DueDatePickerField from './DueDatePickerField' @@ -101,6 +104,11 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { const [hasDescription, setHasDescription] = useState(false) const [hasSubTasks, setHasSubTasks] = useState(false) const [deadlineOffset, setDeadlineOffset] = useState(-1) + const [requireApproval, setRequireApproval] = useState(false) + const [completionWindow, setCompletionWindow] = useState(-1) + const [assignStrategy, setAssignStrategy] = useState('keep_last_assigned') + const [isPrivate, setIsPrivate] = useState(false) + const [showAdvanced, setShowAdvanced] = useState(false) const [dueDateOnly, setDueDateOnly] = useState(null) const [dueTime, setDueTime] = useState(null) const [useCustomTime, setUseCustomTime] = useState(false) @@ -599,6 +607,11 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { setAssignees([]) setProjectId(getInitialProject()) setDeadlineOffset(-1) + setRequireApproval(false) + setCompletionWindow(-1) + setAssignStrategy('keep_last_assigned') + setIsPrivate(false) + setShowAdvanced(false) setDueDateOnly(null) setDueTime(null) setUseCustomTime(false) @@ -610,26 +623,22 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { // Handle different assignee scenarios let finalAssignees = assignees let finalAssignedTo = null - let finalAssignStrategy = 'random' + let finalAssignStrategy = assignStrategy if (isAnyoneTask) { - // @Anyone was explicitly used - anyone can do the task finalAssignees = [] finalAssignedTo = null finalAssignStrategy = 'no_assignee' } else if (assignees.length === 0) { - // No assignees and no @Anyone - fallback to current user finalAssignees = [{ userId: userProfile?.id }] finalAssignedTo = userProfile?.id - finalAssignStrategy = 'keep_last_assigned' + finalAssignStrategy = assignStrategy } else if (assignees.length === 1) { - // Single assignee finalAssignedTo = assignees[0].userId - finalAssignStrategy = 'keep_last_assigned' + finalAssignStrategy = assignStrategy } else { - // Multiple assignees finalAssignedTo = null - finalAssignStrategy = 'random' + finalAssignStrategy = assignStrategy } const chore = { @@ -644,6 +653,10 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { priority: priority ? Number(priority) : 0, points: points > -1 ? points : null, deadlineOffset: deadlineOffset < 0 ? null : deadlineOffset, + completionWindow: + completionWindow < 0 || !dueDate ? null : completionWindow, + requireApproval: requireApproval, + isPrivate: isPrivate, status: 0, frequencyType: 'once', frequencyMetadata: {}, @@ -715,7 +728,6 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { footer={ { /> - + {!hasDescription && ( )} {!hasSubTasks && ( )} + setShowAdvanced(v => !v)} + activeCount={ + [ + points > -1, + requireApproval, + completionWindow > -1, + deadlineOffset > -1, + ].filter(Boolean).length + } + emptyDisplay={pickerEmptyDisplay} + /> + 1} + hasAssignees={assignees.length > 0} + isPrivate={isPrivate} + onIsPrivateChange={setIsPrivate} + /> + {hasDescription && ( Description: diff --git a/src/views/components/AdvancedOptionsSection.jsx b/src/views/components/AdvancedOptionsSection.jsx new file mode 100644 index 0000000..81b0324 --- /dev/null +++ b/src/views/components/AdvancedOptionsSection.jsx @@ -0,0 +1,361 @@ +import { + Add, + Approval, + HourglassTop, + Lock, + MoreHoriz, + People, + Remove, + Timer, +} from '@mui/icons-material' +import { + Box, + IconButton, + Input, + Option, + Select, + Switch, + Typography, +} from '@mui/joy' + +const STRATEGY_OPTIONS = [ + { value: 'keep_last_assigned', label: 'Keep same assignee' }, + { value: 'random', label: 'Random' }, + { value: 'least_completed', label: 'Least completed' }, + { value: 'round_robin', label: 'Round robin' }, +] + +const FieldRow = ({ label, description, children }) => ( + + + + {label} + + {description && ( + + {description} + + )} + + + {children} + + +) + +// Trigger button — place this inside the chip/action row +export const AdvancedOptionsTrigger = ({ + open, + onToggle, + activeCount = 0, + emptyDisplay = 'icon-text', +}) => { + const showLabel = emptyDisplay === 'icon-text' || open || activeCount > 0 + + return ( + + 0 + ? 'primary.outlinedBorder' + : 'neutral.outlinedBorder', + bgcolor: + open || activeCount > 0 ? 'primary.softBg' : 'transparent', + color: + open || activeCount > 0 ? 'primary.softColor' : 'text.secondary', + borderRadius: '128px', + minHeight: 40, + px: showLabel ? 1.5 : 0.75, + cursor: 'pointer', + transition: 'all 0.2s ease-in-out', + '&:hover': { + bgcolor: + open || activeCount > 0 + ? 'primary.softHoverBg' + : 'neutral.softHoverBg', + borderColor: + open || activeCount > 0 + ? 'primary.outlinedHoverBorder' + : 'neutral.outlinedHoverBorder', + }, + }} + > + + + More + + + + {activeCount > 0 && ( + + {activeCount} + + )} + + ) +} + +// Panel — place this as a sibling below the description/subtask sections +const AdvancedOptionsSection = ({ + open, + points, + onPointsChange, + requireApproval, + onRequireApprovalChange, + completionWindow, + onCompletionWindowChange, + deadlineOffset, + onDeadlineOffsetChange, + assignStrategy, + onAssignStrategyChange, + isPrivate, + onIsPrivateChange, + hasDueDate, + hasMultipleAssignees, + hasAssignees, +}) => { + const displayPoints = points <= 0 ? 0 : points + + const handleDecrement = () => { + const next = Math.max(0, displayPoints - 1) + onPointsChange(next === 0 ? -1 : next) + } + + const handleIncrement = () => { + onPointsChange(displayPoints + 1) + } + + const handlePointsInput = e => { + const v = parseInt(e.target.value) + if (isNaN(v) || v <= 0) { + onPointsChange(-1) + } else { + onPointsChange(Math.min(v, 9999)) + } + } + + return ( + + + + {/* Points */} + + + + + + + + + + + {/* Require approval */} + + onRequireApprovalChange(e.target.checked)} + /> + + + {/* Privacy */} + + onIsPrivateChange(e.target.checked)} + /> + + + {/* Assignment strategy — only shown when there are multiple assignees */} + {hasMultipleAssignees && ( + + + + )} + + {/* Completion window and deadline — only when due date set */} + {hasDueDate ? ( + <> + + -1 ? completionWindow : ''} + onChange={e => { + const v = parseInt(e.target.value) + onCompletionWindowChange(isNaN(v) ? -1 : Math.max(0, v)) + }} + endDecorator={ + + hrs + + } + sx={{ width: 96 }} + slotProps={{ input: { min: 0 } }} + /> + + + + -1 ? deadlineOffset : ''} + onChange={e => { + const v = parseInt(e.target.value) + onDeadlineOffsetChange(isNaN(v) ? -1 : Math.max(0, v)) + }} + endDecorator={ + + hrs + + } + sx={{ width: 96 }} + slotProps={{ input: { min: 0 } }} + /> + + + ) : ( + + Set a due date to configure completion window and deadline. + + )} + + + + ) +} + +export default AdvancedOptionsSection