228 lines
6.1 KiB
JavaScript
228 lines
6.1 KiB
JavaScript
import * as monthUtils from './months';
|
|
import q from './query';
|
|
|
|
export function getStatus(nextDate, completed, hasTrans) {
|
|
let today = monthUtils.currentDay();
|
|
|
|
if (completed) {
|
|
return 'completed';
|
|
} else if (hasTrans) {
|
|
return 'paid';
|
|
} else if (nextDate === today) {
|
|
return 'due';
|
|
} else if (nextDate > today && nextDate <= monthUtils.addDays(today, 7)) {
|
|
return 'upcoming';
|
|
} else if (nextDate < today) {
|
|
return 'missed';
|
|
} else {
|
|
return 'scheduled';
|
|
}
|
|
}
|
|
|
|
export function getHasTransactionsQuery(schedules) {
|
|
let filters = schedules.map(schedule => {
|
|
let dateCond = schedule._conditions.find(c => c.field === 'date');
|
|
return {
|
|
$and: {
|
|
schedule: schedule.id,
|
|
date: {
|
|
$gte:
|
|
dateCond && dateCond.op === 'is'
|
|
? schedule.next_date
|
|
: monthUtils.subDays(schedule.next_date, 2)
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
return q('transactions')
|
|
.filter({ $or: filters })
|
|
.orderBy({ date: 'desc' })
|
|
.groupBy('schedule')
|
|
.select(['schedule', 'date']);
|
|
}
|
|
|
|
function makeNumberSuffix(num) {
|
|
// Slight abuse of date-fns to turn a number like "1" into the full
|
|
// form "1st" but formatting a date with that number
|
|
return monthUtils.format(new Date(2020, 0, num, 12), 'do');
|
|
}
|
|
|
|
function prettyDayName(day) {
|
|
let days = {
|
|
SU: 'Sunday',
|
|
MO: 'Monday',
|
|
TU: 'Tuesday',
|
|
WE: 'Wednesday',
|
|
TH: 'Thursday',
|
|
FR: 'Friday',
|
|
SA: 'Saturday'
|
|
};
|
|
return days[day];
|
|
}
|
|
|
|
export function getRecurringDescription(config) {
|
|
let interval = config.interval || 1;
|
|
|
|
switch (config.frequency) {
|
|
case 'weekly': {
|
|
let desc = 'Every ';
|
|
desc += interval !== 1 ? `${interval} weeks` : 'week';
|
|
desc += ' on ' + monthUtils.format(config.start, 'EEEE');
|
|
return desc;
|
|
}
|
|
case 'monthly': {
|
|
let desc = 'Every ';
|
|
desc += interval !== 1 ? `${interval} months` : 'month';
|
|
|
|
if (config.patterns && config.patterns.length > 0) {
|
|
// Sort the days ascending. We filter out -1 because that
|
|
// represents "last days" and should always be last, but this
|
|
// sort would put them first
|
|
let patterns = [...config.patterns]
|
|
.sort((p1, p2) => {
|
|
let typeOrder =
|
|
(p1.type === 'day' ? 1 : 0) - (p2.type === 'day' ? 1 : 0);
|
|
let valOrder = p1.value - p2.value;
|
|
|
|
if (typeOrder === 0) {
|
|
return valOrder;
|
|
}
|
|
return typeOrder;
|
|
})
|
|
.filter(p => p.value !== -1);
|
|
|
|
// Add on all -1 values to the end
|
|
patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
|
|
|
|
desc += ' on the ';
|
|
|
|
let strs = [];
|
|
|
|
let uniqueDays = new Set(patterns.map(p => p.type));
|
|
let isSameDay = uniqueDays.length === 1 && !uniqueDays.has('day');
|
|
|
|
for (let pattern of patterns) {
|
|
if (pattern.type === 'day') {
|
|
if (pattern.value === -1) {
|
|
strs.push('last day');
|
|
} else {
|
|
// Example: 15th day
|
|
strs.push(makeNumberSuffix(pattern.value));
|
|
}
|
|
} else {
|
|
let dayName = isSameDay ? '' : ' ' + prettyDayName(pattern.type);
|
|
|
|
if (pattern.value === -1) {
|
|
// Example: last Monday
|
|
strs.push('last' + dayName);
|
|
} else {
|
|
// Example: 3rd Monday
|
|
strs.push(makeNumberSuffix(pattern.value) + dayName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strs.length > 2) {
|
|
desc += strs.slice(0, strs.length - 1).join(', ');
|
|
desc += ', and ';
|
|
desc += strs[strs.length - 1];
|
|
} else {
|
|
desc += strs.join(' and ');
|
|
}
|
|
|
|
if (isSameDay) {
|
|
desc += ' ' + prettyDayName(patterns[0].type);
|
|
}
|
|
} else {
|
|
desc += ' on the ' + monthUtils.format(config.start, 'do');
|
|
}
|
|
|
|
return desc;
|
|
}
|
|
case 'yearly': {
|
|
let desc = 'Every ';
|
|
desc += interval !== 1 ? `${interval} years` : 'year';
|
|
desc += ' on ' + monthUtils.format(config.start, 'LLL do');
|
|
return desc;
|
|
}
|
|
default:
|
|
return 'Recurring error';
|
|
}
|
|
}
|
|
|
|
export function recurConfigToRSchedule(config) {
|
|
let base = {
|
|
start: monthUtils.parseDate(config.start),
|
|
frequency: config.frequency.toUpperCase(),
|
|
byHourOfDay: [12]
|
|
};
|
|
|
|
if (config.interval) {
|
|
base.interval = config.interval;
|
|
}
|
|
|
|
let abbrevDay = name => name.slice(0, 2).toUpperCase();
|
|
|
|
switch (config.frequency) {
|
|
case 'weekly':
|
|
// Nothing to do
|
|
return [base];
|
|
case 'monthly':
|
|
if (config.patterns && config.patterns.length > 0) {
|
|
let days = config.patterns.filter(p => p.type === 'day');
|
|
let dayNames = config.patterns.filter(p => p.type !== 'day');
|
|
|
|
return [
|
|
days.length > 0 && { ...base, byDayOfMonth: days.map(p => p.value) },
|
|
dayNames.length > 0 && {
|
|
...base,
|
|
byDayOfWeek: dayNames.map(p => [abbrevDay(p.type), p.value])
|
|
}
|
|
].filter(Boolean);
|
|
} else {
|
|
// Nothing to do
|
|
return [base];
|
|
}
|
|
case 'yearly':
|
|
return [base];
|
|
default:
|
|
throw new Error('Invalid recurring date config');
|
|
}
|
|
}
|
|
|
|
export function extractScheduleConds(conditions) {
|
|
return {
|
|
payee:
|
|
conditions.find(cond => cond.op === 'is' && cond.field === 'payee') ||
|
|
conditions.find(
|
|
cond => cond.op === 'is' && cond.field === 'description'
|
|
) ||
|
|
null,
|
|
account:
|
|
conditions.find(cond => cond.op === 'is' && cond.field === 'account') ||
|
|
conditions.find(cond => cond.op === 'is' && cond.field === 'acct') ||
|
|
null,
|
|
amount:
|
|
conditions.find(
|
|
cond =>
|
|
(cond.op === 'is' ||
|
|
cond.op === 'isapprox' ||
|
|
cond.op === 'isbetween') &&
|
|
cond.field === 'amount'
|
|
) || null,
|
|
date:
|
|
conditions.find(
|
|
cond =>
|
|
(cond.op === 'is' || cond.op === 'isapprox') && cond.field === 'date'
|
|
) || null
|
|
};
|
|
}
|
|
|
|
export function getScheduledAmount(amount) {
|
|
if (amount && typeof amount !== 'number') {
|
|
return Math.round((amount.num1 + amount.num2) / 2);
|
|
}
|
|
return amount;
|
|
}
|