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) => {