378 lines
10 KiB
JavaScript
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
|
|
})
|
|
}
|