From 343245f29047e64b9fd7807c35d2d2a9fee492db Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Mon, 9 Mar 2026 01:10:00 -0400 Subject: [PATCH] Add Smart Insights feature to Sidepanel and refactor SidepanelSettings --- src/utils/SidepanelConfig.js | 36 ++- src/views/Chores/Sidepanel.jsx | 19 +- src/views/Chores/SmartInsightsCard.jsx | 379 +++++++++++++++++++++++ src/views/Settings/SidepanelSettings.jsx | 64 +--- 4 files changed, 444 insertions(+), 54 deletions(-) create mode 100644 src/views/Chores/SmartInsightsCard.jsx diff --git a/src/utils/SidepanelConfig.js b/src/utils/SidepanelConfig.js index ed065d3..26ffb8e 100644 --- a/src/utils/SidepanelConfig.js +++ b/src/utils/SidepanelConfig.js @@ -7,13 +7,21 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ enabled: true, order: 0, }, + { + id: 'smartInsights', + name: 'Smart Insights', + description: 'Quick actions based on your tasks', + iconName: 'TrendingUp', + enabled: false, + order: 1, + }, { id: 'assignees', name: 'Tasks by Assignee', description: 'Groups tasks by who they are assigned to', iconName: 'Person', enabled: true, - order: 1, + order: 2, }, { id: 'calendar', @@ -21,7 +29,7 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows tasks in a calendar format', iconName: 'CalendarMonth', enabled: true, - order: 2, + order: 3, }, { id: 'activities', @@ -29,7 +37,7 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows recent task completions and activities', iconName: 'History', enabled: true, - order: 3, + order: 4, }, { id: 'weeklyGoals', @@ -37,21 +45,37 @@ export const DEFAULT_SIDEPANEL_CONFIG = [ description: 'Shows weekly progress and family completion stats', iconName: 'EmojiEvents', enabled: true, - order: 4, + order: 5, }, ] export const getSidepanelConfig = () => { const saved = localStorage.getItem('sidepanelConfig') + let savedConfig = [] + if (saved) { try { - return JSON.parse(saved) + savedConfig = JSON.parse(saved) } catch (error) { console.error('Error parsing sidepanel config:', error) return DEFAULT_SIDEPANEL_CONFIG } } - return DEFAULT_SIDEPANEL_CONFIG + + // Merge saved config with default config + // This ensures new items in DEFAULT_SIDEPANEL_CONFIG are added to existing configs + const mergedConfig = DEFAULT_SIDEPANEL_CONFIG.map(defaultItem => { + const savedItem = savedConfig.find(item => item.id === defaultItem.id) + return savedItem || defaultItem + }) + + // Add any saved items that are no longer in default (for backwards compatibility) + const newSavedItems = savedConfig.filter( + savedItem => + !DEFAULT_SIDEPANEL_CONFIG.find(item => item.id === savedItem.id), + ) + + return [...mergedConfig, ...newSavedItems] } export const saveSidepanelConfig = config => { diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx index b551f1b..a045249 100644 --- a/src/views/Chores/Sidepanel.jsx +++ b/src/views/Chores/Sidepanel.jsx @@ -6,10 +6,17 @@ import { ChoresGrouper } from '../../utils/Chores' import { getSidepanelConfig } from '../../utils/SidepanelConfig' import CalendarCard from '../components/CalendarCard' import ActivitiesCard from './ActivitesCard' +import SmartInsightsCard from './SmartInsightsCard' import TasksByAssigneeCard from './TasksByAssigneeCard' import UserSwitcher from './UserSwitcher' -const Sidepanel = ({ chores }) => { +const Sidepanel = ({ + chores, + allChores, + applyTempFilter, + clearTempFilter, + tempFilter, +}) => { const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg')) const [dueDatePieChartData, setDueDatePieChartData] = useState([]) const [sidepanelConfig, setSidepanelConfig] = useState([]) @@ -55,6 +62,16 @@ const Sidepanel = ({ chores }) => { switch (cardConfig.id) { case 'welcome': return + case 'smartInsights': + return ( + + ) case 'assignees': return case 'calendar': diff --git a/src/views/Chores/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx new file mode 100644 index 0000000..0e1b666 --- /dev/null +++ b/src/views/Chores/SmartInsightsCard.jsx @@ -0,0 +1,379 @@ +import { + EventBusy, + EventNote, + HourglassEmpty, + PriorityHigh, + TrendingUp, + WatchLater, +} from '@mui/icons-material' +import { Box, Button, Chip, Sheet, Typography } from '@mui/joy' +import { useMemo } from 'react' +import { TASK_COLOR } from '../../utils/Colors' + +// Static insight filter definitions – used for URL restoration +export const INSIGHT_FILTER_DEFS = { + overdue: { + name: 'Overdue', + filter: { + conditions: [{ type: 'dueDate', operator: 'isOverdue', value: null }], + operator: 'AND', + }, + }, + 'due-today': { + name: 'Due Today', + filter: { + conditions: [{ type: 'dueDate', operator: 'isDueToday', value: null }], + operator: 'AND', + }, + }, + 'pending-approval': { + name: 'Pending Approval', + filter: { + conditions: [{ type: 'status', operator: 'is', value: 3 }], + operator: 'AND', + }, + }, + 'due-this-week': { + name: 'Due This Week', + filter: { + conditions: [{ type: 'dueDate', operator: 'isDueThisWeek', value: null }], + operator: 'AND', + }, + }, + 'high-priority': { + name: 'High Priority', + filter: { + conditions: [{ type: 'priority', operator: 'is', value: [1, 2] }], + operator: 'AND', + }, + }, + 'no-due-date': { + name: 'No Due Date', + filter: { + conditions: [{ type: 'dueDate', operator: 'hasNoDueDate', value: null }], + operator: 'AND', + }, + }, +} + +const SmartInsightsCard = ({ + chores, + applyTempFilter, + clearTempFilter, + tempFilter, +}) => { + // Detect all possible insights from chores + const insights = useMemo(() => { + if (!chores || chores.length === 0) return [] + + const now = new Date() + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + const nextWeek = new Date(today) + nextWeek.setDate(nextWeek.getDate() + 7) + + const detectedInsights = [] + + // 1. Overdue tasks (Highest Priority) + const overdueTasks = chores.filter( + chore => chore.nextDueDate && new Date(chore.nextDueDate) < now, + ) + if (overdueTasks.length > 0) { + detectedInsights.push({ + id: 'overdue', + priority: 1, + count: overdueTasks.length, + title: 'Overdue', + description: `${overdueTasks.length} ${overdueTasks.length === 1 ? 'task is' : 'tasks are'} overdue`, + color: 'danger', + bgColor: TASK_COLOR.OVERDUE, + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isOverdue', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 2. Due today (High Priority) + const dueTodayTasks = chores.filter( + chore => + chore.nextDueDate && + new Date(chore.nextDueDate).toDateString() === today.toDateString(), + ) + if (dueTodayTasks.length > 0) { + detectedInsights.push({ + id: 'due-today', + priority: 2, + count: dueTodayTasks.length, + title: 'Due Today', + description: `${dueTodayTasks.length} ${dueTodayTasks.length === 1 ? 'task' : 'tasks'} due by end of day`, + color: 'warning', + bgColor: '#FFA500', + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isDueToday', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 3. Pending approval (High Priority) + const pendingApprovalTasks = chores.filter(chore => chore.status === 3) + if (pendingApprovalTasks.length > 0) { + detectedInsights.push({ + id: 'pending-approval', + priority: 3, + count: pendingApprovalTasks.length, + title: 'Pending Approval', + description: `${pendingApprovalTasks.length} ${pendingApprovalTasks.length === 1 ? 'task awaits' : 'tasks await'} approval`, + color: 'neutral', + bgColor: TASK_COLOR.PENDING_REVIEW, + icon: , + filter: { + conditions: [ + { + type: 'status', + operator: 'is', + value: 3, + }, + ], + operator: 'AND', + }, + }) + } + + // 4. Due this week (excluding today) (Medium Priority) + const dueThisWeekTasks = chores.filter(chore => { + if (!chore.nextDueDate) return false + const dueDate = new Date(chore.nextDueDate) + return dueDate >= tomorrow && dueDate < nextWeek + }) + if (dueThisWeekTasks.length > 0) { + detectedInsights.push({ + id: 'due-this-week', + priority: 4, + count: dueThisWeekTasks.length, + title: 'Due This Week', + description: `${dueThisWeekTasks.length} ${dueThisWeekTasks.length === 1 ? 'task' : 'tasks'} due in the next 7 days`, + color: 'primary', + bgColor: TASK_COLOR.IN_PROGRESS, + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'isDueThisWeek', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // 5. High priority tasks (Medium Priority) + const highPriorityTasks = chores.filter( + chore => chore.priority === 1 || chore.priority === 2, + ) + if (highPriorityTasks.length > 0) { + detectedInsights.push({ + id: 'high-priority', + priority: 5, + count: highPriorityTasks.length, + title: 'High Priority', + description: `${highPriorityTasks.length} ${highPriorityTasks.length === 1 ? 'task requires' : 'tasks require'} immediate attention`, + color: 'warning', + bgColor: '#FF6B6B', + icon: , + filter: { + conditions: [ + { + type: 'priority', + operator: 'is', + value: [1, 2], + }, + ], + operator: 'AND', + }, + }) + } + + // 6. No due date (Lower Priority) + const noDueDateTasks = chores.filter( + chore => !chore.nextDueDate || chore.nextDueDate === null, + ) + if (noDueDateTasks.length > 0) { + detectedInsights.push({ + id: 'no-due-date', + priority: 6, + count: noDueDateTasks.length, + title: 'No Due Date', + description: `${noDueDateTasks.length} ${noDueDateTasks.length === 1 ? 'task needs' : 'tasks need'} a deadline`, + color: 'neutral', + bgColor: '#9E9E9E', + icon: , + filter: { + conditions: [ + { + type: 'dueDate', + operator: 'hasNoDueDate', + value: null, + }, + ], + operator: 'AND', + }, + }) + } + + // Sort by priority and return top 3 + return detectedInsights.sort((a, b) => a.priority - b.priority).slice(0, 3) + }, [chores]) + + const handleInsightClick = insight => { + // Toggle: if already active, clear it; otherwise apply it + if (isInsightActive(insight)) { + clearTempFilter() + } else { + applyTempFilter(insight.filter, { + id: insight.id, + name: insight.title, + description: insight.description, + icon: insight.icon, + color: insight.color, + }) + } + } + + const isInsightActive = insight => { + if (!tempFilter || !tempFilter.conditions) return false + return ( + JSON.stringify(tempFilter.conditions) === + JSON.stringify(insight.filter.conditions) + ) + } + + if (insights.length === 0) { + return null + } + + return ( + + {/* Header */} + + + + + Smart Insights + + {tempFilter && ( + + Active + + )} + + + {tempFilter + ? 'Click active filter to clear' + : 'Quick actions based on your tasks'} + + + + {/* Insight Cards */} + + {insights.map(insight => { + const isActive = isInsightActive(insight) + return ( + + ) + })} + + + ) +} + +export default SmartInsightsCard diff --git a/src/views/Settings/SidepanelSettings.jsx b/src/views/Settings/SidepanelSettings.jsx index 0734da7..6467858 100644 --- a/src/views/Settings/SidepanelSettings.jsx +++ b/src/views/Settings/SidepanelSettings.jsx @@ -2,8 +2,11 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd' import { CalendarMonth, DragIndicator, + EmojiEvents, History, Person, + SupervisorAccount, + TrendingUp, Visibility, VisibilityOff, WavingHand, @@ -23,48 +26,22 @@ import { Typography, } from '@mui/joy' import { useEffect, useState } from 'react' +import { + DEFAULT_SIDEPANEL_CONFIG, + getSidepanelConfig, + saveSidepanelConfig, +} from '../../utils/SidepanelConfig' import SettingsLayout from './SettingsLayout' -const DEFAULT_SIDEPANEL_CONFIG = [ - { - id: 'welcome', - name: 'Welcome Card', - description: 'Shows greeting and quick stats', - iconName: 'WavingHand', - enabled: true, - order: 0, - }, - { - id: 'assignees', - name: 'Tasks by Assignee', - description: 'Groups tasks by who they are assigned to', - iconName: 'Person', - enabled: true, - order: 1, - }, - { - id: 'calendar', - name: 'Calendar View', - description: 'Shows tasks in a calendar format', - iconName: 'CalendarMonth', - enabled: true, - order: 2, - }, - { - id: 'activities', - name: 'Recent Activities', - description: 'Shows recent task completions and activities', - iconName: 'History', - enabled: true, - order: 3, - }, -] - const SidepanelSettings = () => { - const [config, setConfig] = useState(DEFAULT_SIDEPANEL_CONFIG) + const [config, setConfig] = useState(getSidepanelConfig()) const getIcon = iconName => { switch (iconName) { + case 'SupervisorAccount': + return + case 'TrendingUp': + return case 'WavingHand': return case 'Person': @@ -73,27 +50,20 @@ const SidepanelSettings = () => { return case 'History': return + case 'EmojiEvents': + return default: return } } useEffect(() => { - const saved = localStorage.getItem('sidepanelConfig') - if (saved) { - try { - const parsed = JSON.parse(saved) - setConfig(parsed) - } catch (error) { - console.error('Error parsing sidepanel config:', error) - } - } + setConfig(getSidepanelConfig()) }, []) const saveConfig = newConfig => { setConfig(newConfig) - localStorage.setItem('sidepanelConfig', JSON.stringify(newConfig)) - window.dispatchEvent(new Event('sidepanelConfigChanged')) + saveSidepanelConfig(newConfig) } const handleToggleEnabled = (id, enabled) => {