Files
donetick/src/utils/Chores.jsx
2026-08-16 11:52:40 -04:00

378 lines
10 KiB
JavaScript

import moment from 'moment'
import { TASK_COLOR } from './Colors.jsx'
const priorityOrder = [1, 2, 3, 4, 0]
// ChoreGrouperOptions enum:
export const GROUPING_OPTIONS = Object.freeze({
SMART: 'default',
DUE_DATE: 'due_date',
PRIORITY: 'priority',
LABELS: 'labels',
})
export const ChoreHistoryStatus = Object.freeze({
STARTED: 0,
COMPLETED: 1,
SKIPPED: 2,
PENDING_APPROVAL: 3,
REJECTED: 4,
MISSED: 5,
RESCHEDULED: 6,
})
export const ChoreStatus = Object.freeze({
INACTIVE: 0,
ACTIVE: 1,
PAUSED: 2,
PENDING_APPROVAL: 3,
})
const getDateGroupKey = dueDate =>
moment(dueDate).startOf('day').format('YYYY-MM-DD')
const getDateGroupName = dateKey =>
moment(dateKey, 'YYYY-MM-DD').format('dddd, MMM D')
const getDateGroupColor = dateKey => {
const today = moment().startOf('day')
const tomorrow = moment().add(1, 'day').startOf('day')
const groupDate = moment(dateKey, 'YYYY-MM-DD')
if (groupDate.isBefore(today)) {
return TASK_COLOR.OVERDUE
}
if (groupDate.isSame(today)) {
return TASK_COLOR.TODAY
}
if (groupDate.isSame(tomorrow)) {
return TASK_COLOR.TOMORROW
}
if (groupDate.isBefore(moment(today).add(8, 'days'))) {
return TASK_COLOR.NEXT_7_DAYS
}
if (groupDate.isSame(today, 'month')) {
return TASK_COLOR.LATER_THIS_MONTH
}
return TASK_COLOR.FUTURE
}
const buildActualDateGroups = chores => {
const groupedByDate = {}
const anytime = []
chores.forEach(chore => {
if (!chore.nextDueDate) {
anytime.push(chore)
return
}
const dateKey = getDateGroupKey(chore.nextDueDate)
if (!groupedByDate[dateKey]) {
groupedByDate[dateKey] = []
}
groupedByDate[dateKey].push(chore)
})
const dateGroups = Object.keys(groupedByDate)
.sort(
(a, b) =>
moment(a, 'YYYY-MM-DD').valueOf() - moment(b, 'YYYY-MM-DD').valueOf(),
)
.map(dateKey => ({
name: getDateGroupName(dateKey),
content: groupedByDate[dateKey],
color: getDateGroupColor(dateKey),
}))
return { dateGroups, anytime }
}
export const ChoresGrouper = (groupBy, chores, filter) => {
if (filter) {
chores = chores.filter(chore => filter(chore))
} else {
chores = [...chores]
}
// sort by priority then due date:
chores.sort(ChoreSorter)
var groups = []
switch (groupBy) {
case 'default': {
// same as due_date but hide empty groups: and if status is 1 or 2 have seperated catigory as Started:
var groupRaw = {
PendingApproval: [],
Started: [],
Today: [],
Tomorrow: [],
'Next 7 Days': [],
'Later This Month': [],
Future: [],
Overdue: [],
Anytime: [],
}
chores.forEach(chore => {
if (chore.status === 1 || chore.status === 2) {
groupRaw['Started'].push(chore)
} else if (chore.status === 3) {
groupRaw['PendingApproval'].push(chore)
} else if (chore.nextDueDate === null) {
groupRaw['Anytime'].push(chore)
} else if (new Date(chore.nextDueDate) < new Date()) {
groupRaw['Overdue'].push(chore)
} else if (
new Date(chore.nextDueDate).toDateString() ===
new Date().toDateString()
) {
groupRaw['Today'].push(chore)
} else if (
new Date(chore.nextDueDate).toDateString() ===
new Date(Date.now() + 24 * 60 * 60 * 1000).toDateString()
) {
groupRaw['Tomorrow'].push(chore)
} else if (
new Date(chore.nextDueDate) <
new Date(Date.now() + 8 * 24 * 60 * 60 * 1000) &&
new Date(chore.nextDueDate) >
new Date(Date.now() + 24 * 60 * 60 * 1000)
) {
groupRaw['Next 7 Days'].push(chore)
} else if (
new Date(chore.nextDueDate).getMonth() === new Date().getMonth() &&
new Date(chore.nextDueDate).getFullYear() === new Date().getFullYear()
) {
groupRaw['Later This Month'].push(chore)
} else {
groupRaw['Future'].push(chore)
}
})
groups = []
if (groupRaw['Started'].length > 0) {
groups.push({
name: 'Started',
content: groupRaw['Started'],
color: TASK_COLOR.STARTED,
})
}
if (groupRaw['PendingApproval'].length > 0) {
groups.push({
name: 'Pending Approval',
content: groupRaw['PendingApproval'],
color: TASK_COLOR.LATE,
})
}
if (groupRaw['Overdue'].length > 0) {
groups.push({
name: 'Overdue',
content: groupRaw['Overdue'],
color: TASK_COLOR.OVERDUE,
})
}
if (groupRaw['Today'].length > 0) {
groups.push({
name: 'Today',
content: groupRaw['Today'],
color: TASK_COLOR.TODAY,
})
}
if (groupRaw['Tomorrow'].length > 0) {
groups.push({
name: 'Tomorrow',
content: groupRaw['Tomorrow'],
color: TASK_COLOR.TOMORROW,
})
}
if (groupRaw['Next 7 Days'].length > 0) {
groups.push({
name: 'Next 7 Days',
content: groupRaw['Next 7 Days'],
color: TASK_COLOR.NEXT_7_DAYS,
})
}
if (groupRaw['Later This Month'].length > 0) {
groups.push({
name: 'Later This Month',
content: groupRaw['Later This Month'],
color: TASK_COLOR.LATER_THIS_MONTH,
})
}
if (groupRaw['Future'].length > 0) {
groups.push({
name: 'Future',
content: groupRaw['Future'],
color: TASK_COLOR.FUTURE,
})
}
if (groupRaw['Anytime'].length > 0) {
groups.push({
name: 'Anytime',
content: groupRaw['Anytime'],
color: TASK_COLOR.ANYTIME,
})
}
break
}
case 'due_date': {
var { anytime: dueAnytime, dateGroups: dueDateGroups } =
buildActualDateGroups(chores)
groups = [...dueDateGroups]
if (dueAnytime.length > 0) {
groups.push({
name: 'Anytime',
content: dueAnytime,
color: TASK_COLOR.ANYTIME,
})
}
break
}
case 'priority':
groupRaw = {
p1: [],
p2: [],
p3: [],
p4: [],
no_priority: [],
}
chores.forEach(chore => {
switch (chore.priority) {
case 1:
groupRaw['p1'].push(chore)
break
case 2:
groupRaw['p2'].push(chore)
break
case 3:
groupRaw['p3'].push(chore)
break
case 4:
groupRaw['p4'].push(chore)
break
default:
groupRaw['no_priority'].push(chore)
break
}
})
groups = [
{
name: 'Priority 1',
content: groupRaw['p1'],
color: TASK_COLOR.PRIORITY_1,
},
{
name: 'Priority 2',
content: groupRaw['p2'],
color: TASK_COLOR.PRIORITY_2,
},
{
name: 'Priority 3',
content: groupRaw['p3'],
color: TASK_COLOR.PRIORITY_3,
},
{
name: 'Priority 4',
content: groupRaw['p4'],
color: TASK_COLOR.PRIORITY_4,
},
{
name: 'No Priority',
content: groupRaw['no_priority'],
color: TASK_COLOR.NO_PRIORITY,
},
]
break
case 'labels':
groupRaw = {}
var labels = {}
chores.forEach(chore => {
chore.labelsV2.forEach(label => {
labels[label.id] = label
if (groupRaw[label.id] === undefined) {
groupRaw[label.id] = []
}
groupRaw[label.id].push(chore)
})
})
groups = Object.keys(groupRaw).map(key => {
return {
name: labels[key].name,
content: groupRaw[key],
}
})
groups.sort((a, b) => {
a.name < b.name ? 1 : -1
})
}
return groups
}
export const ChoreSorter = (a, b) => {
const priorityA = priorityOrder.indexOf(a.priority)
const priorityB = priorityOrder.indexOf(b.priority)
if (priorityA !== priorityB) {
return priorityA - priorityB
}
// Status sorting (0 > 1 > ... ascending order)
if (a.status !== b.status) {
return a.status - b.status
}
// Due date sorting (earlier dates first, null/undefined last)
if (!a.nextDueDate && !b.nextDueDate) return 0
if (!a.nextDueDate) return 1
if (!b.nextDueDate) return -1
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
}
export const notInCompletionWindow = chore => {
return (
chore.completionWindow &&
chore.completionWindow > -1 &&
chore.nextDueDate &&
moment() < moment(chore.nextDueDate).add(-chore.completionWindow, 'hours')
)
}
export const ChoreFilters = userId => ({
anyone: () => true,
assigned_to_me: chore => {
return chore.assignedTo && chore.assignedTo === userId
},
available_for_me: chore => {
return chore.assignedTo === null || chore.assignedTo === userId
},
assigned_to_others: chore => {
return chore.assignedTo && chore.assignedTo !== userId
},
assigned_to_me_tasks: chore => {
return (
chore.assignees &&
chore.assignees.some(assignee => assignee.userId === userId)
)
},
created_by_me: chore => {
return chore.createdBy && chore.createdBy === userId
},
})
// Project filter function - separate from ChoreFilters since it's independent
export const filterByProject = (chores, selectedProject) => {
if (
!selectedProject ||
selectedProject === 'Default Project' ||
selectedProject === 'default'
) {
// Default project should show tasks without a project (projectId is null/undefined/empty)
// Based on ChoreEdit.jsx, default projects save projectId as null
return chores.filter(
chore =>
!chore.projectId || chore.projectId === null || chore.projectId === '',
)
}
// For custom projects, match by project ID
return chores.filter(chore => {
// Match by project ID (this should be the primary way chores are linked to projects)
return chore.projectId === selectedProject
})
}