Skip to content

Commit

Permalink
Also show automatically targeted groupings in expenses listing
Browse files Browse the repository at this point in the history
  • Loading branch information
thaapasa committed Mar 3, 2024
1 parent d6b945b commit 6e2effd
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 27 deletions.
12 changes: 12 additions & 0 deletions src/client/ui/expense/row/ExpenseRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export class ExpenseRowImpl extends React.Component<ExpenseRowProps, ExpenseRowS
}
const firstDay = !this.props.prev || !toDayjs(expense.date).isSame(this.props.prev.date, 'day');
const grouping = this.props.groupingMap[this.props.expense?.groupingId ?? 0];
const autoGroupings = (this.props.expense?.autoGroupingIds ?? [])
.map(g => this.props.groupingMap[g])
.filter(isDefined);
return (
<>
<Row className={firstDay && this.props.dateBorder ? 'first-day' : ''}>
Expand Down Expand Up @@ -241,6 +244,15 @@ export class ExpenseRowImpl extends React.Component<ExpenseRowProps, ExpenseRowS
this.props.addFilter(e => e.groupingId === grouping.id, grouping.title)
}
/>
) : autoGroupings ? (
autoGroupings.map(a => (
<GroupedExpenseIcon
key={a.id}
grouping={a}
implicit={true}
onClick={() => this.props.addFilter(e => e.groupingId === a.id, a.title)}
/>
))
) : null}
</IconToolArea>
<ActivatableTextField
Expand Down
14 changes: 12 additions & 2 deletions src/client/ui/grouping/GroupedExpenseIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ type GroupedExpenseIconProps = {
grouping: ExpenseGroupingRef;
onClick?: () => void;
className?: string;
implicit?: boolean;
};

export const GroupedExpenseIcon: React.FC<GroupedExpenseIconProps> = ({
size,
grouping,
onClick,
className,
implicit,
}) => {
const color = grouping.color ?? colors.blue[300];
const luminance = getLuminanceSafe(color);

return (
<IconContainer title={grouping.title} onClick={onClick} className={className}>
<Bookmark size={size || 24} title={grouping.title} color={color} />
<IconContainer
title={grouping.title}
onClick={onClick}
className={className + (implicit ? ' implicit' : '')}
>
<Bookmark size={size || 24} title={grouping.title} color={color} outline={implicit} />
{grouping.title ? (
<GroupedExpenseIconText color={luminance > 0.4 ? 'black' : 'white'}>
{grouping.title[0]}
Expand All @@ -39,6 +45,10 @@ const IconContainer = styled('div')`
cursor: pointer;
position: relative;
display: inline-block;
&.implicit {
// opacity: 0.5;
}
`;

const GroupedExpenseIconText = styled('div')`
Expand Down
10 changes: 7 additions & 3 deletions src/client/ui/icons/Bookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ export const Bookmark: React.FC<{
title: string;
onClick?: () => void;
color?: string;
}> = ({ size: height, title, onClick, color }) => {
outline?: boolean;
}> = ({ size: height, title, onClick, color, outline }) => {
const width = (height * 14.0) / 18;
const col = color ?? colorScheme.primary.dark;
return (
<svg width={width + 'px'} height={height + 'px'} viewBox="0 0 14 18" onClick={onClick}>
<svg width={width + 'px'} height={height + 'px'} viewBox="0 0 14.5 18" onClick={onClick}>
<title>{title}</title>
<path
d="M14,2 L14,18 L7,15 L0,18 L0.006875,7 L0,7 L0,0 L14,0 L14,2 Z"
fill={color ?? colorScheme.primary.dark}
fill={outline ? `${col}77` : col}
stroke={outline ? col : undefined}
strokeWidth={outline ? 1 : undefined}
/>
</svg>
);
Expand Down
27 changes: 19 additions & 8 deletions src/server/data/BasicExpenseDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
UserExpense,
} from 'shared/expense';
import { DateLike, toISODate } from 'shared/time';
import { ApiMessage, NotFoundError, ObjectId } from 'shared/types';
import { ApiMessage, isDefined, NotFoundError, ObjectId } from 'shared/types';
import { Money, MoneyLike } from 'shared/util';
import { logger } from 'server/Logger';

Expand All @@ -27,15 +27,15 @@ export function expenseSelectClause(
SELECT
MIN(id) AS id, MIN(date) AS date, MIN(receiver) AS receiver, MIN(type) AS type, MIN(sum) AS sum,
MIN(title) AS title, MIN(description) AS description, BOOL_AND(confirmed) AS confirmed, MIN(source_id) AS "sourceId",
MIN(user_id) AS "userId", MIN(created_by_id) AS "createdById", MIN(group_id) AS "groupId", MIN(category_id) AS "categoryId",
MIN(grouping_id) AS "groupingId",
MIN(user_id) AS "userId", MIN(created_by_id) AS "createdById", MIN(breakdown.group_id) AS "groupId", MIN(category_id) AS "categoryId",
MIN(grouping_id) AS "groupingId", ARRAY_AGG(DISTINCT auto_grouping_id) AS "autoGroupingIds",
MIN(created) AS created, MIN(recurring_expense_id) AS "recurringExpenseId",
SUM(cost) AS "userCost", SUM(benefit) AS "userBenefit", SUM(income) AS "userIncome", SUM(split) AS "userSplit",
SUM(transferor) AS "userTransferor", SUM(transferee) AS "userTransferee",
SUM(cost + benefit + income + split + transferor + transferee) AS "userValue"
FROM (
SELECT
e.id, e.date::DATE, e.receiver, e.type, e.sum, e.title, e.description, e.confirmed, e.grouping_id,
e.id, e.date::DATE, e.receiver, e.type, e.sum, e.title, e.description, e.confirmed, e.grouping_id, eg.id AS "auto_grouping_id",
e.source_id, e.user_id, e.created_by_id, e.group_id, e.category_id, e.created, e.recurring_expense_id,
(CASE WHEN d.type = 'cost' THEN d.sum ELSE '0.00'::NUMERIC END) AS cost,
(CASE WHEN d.type = 'benefit' THEN d.sum ELSE '0.00'::NUMERIC END) AS benefit,
Expand All @@ -45,6 +45,16 @@ FROM (
(CASE WHEN d.type = 'transferee' THEN d.sum ELSE '0.00'::NUMERIC END) AS transferee
FROM expenses e
LEFT JOIN expense_division d ON (d.expense_id = e.id AND d.user_id = $/userId/)
LEFT JOIN categories cat ON (cat.id = e.category_id)
LEFT JOIN expense_groupings eg ON ((eg.id = e.grouping_id) OR (
eg.group_id = e.group_id
AND eg.id IN (
SELECT expense_grouping_id FROM expense_grouping_categories egc WHERE egc.category_id IN (cat.id, cat.parent_id)
AND (eg.start_date IS NULL OR eg.start_date <= e.date)
AND (eg.end_date IS NULL OR eg.end_date >= e.date)
AND (eg.only_own IS FALSE or e.user_id=$/userId/)
))
)
${where}
) breakdown
GROUP BY id
Expand All @@ -70,14 +80,15 @@ FROM (
(CASE WHEN d.type = 'transferee' THEN d.sum ELSE '0.00'::NUMERIC END) AS transferee
FROM expenses e
LEFT JOIN expense_division d ON (d.expense_id = e.id AND d.user_id = $/userId/::INTEGER)
WHERE group_id=$/groupId/::INTEGER AND template=false AND date >= $/startDate/::DATE AND date < $/endDate/::DATE
WHERE e.group_id=$/groupId/::INTEGER AND template=false AND date >= $/startDate/::DATE AND date < $/endDate/::DATE
) breakdown
`;

export function dbRowToExpense(e: UserExpense): UserExpense {
if (!e) {
throw new NotFoundError('EXPENSE_NOT_FOUND', 'expense');
}
e.autoGroupingIds = (e.autoGroupingIds ?? []).filter(isDefined);
e.date = toISODate(e.date);
e.userBalance = Money.from(e.userValue).negate().toString();
e.groupingId = e.groupingId ?? undefined;
Expand All @@ -90,7 +101,7 @@ export async function getAllExpenses(
userId: number,
): Promise<Expense[]> {
const expenses = await tx.map(
expenseSelectClause(`WHERE group_id=$/groupId/`),
expenseSelectClause(`WHERE e.group_id=$/groupId/`),
{ userId, groupId },
dbRowToExpense,
);
Expand Down Expand Up @@ -133,7 +144,7 @@ export async function getExpenseById(
expenseId: number,
): Promise<UserExpense> {
const expense = await tx.map(
expenseSelectClause(`WHERE id=$/expenseId/ AND group_id=$/groupId/`),
expenseSelectClause(`WHERE e.id=$/expenseId/ AND e.group_id=$/groupId/`),
{ userId, expenseId, groupId },
dbRowToExpense,
);
Expand Down Expand Up @@ -326,7 +337,7 @@ async function getRecurrenceOccurence(
): Promise<Expense | undefined> {
const expense = await tx.map(
expenseSelectClause(
`WHERE recurring_expense_id=$/recurringExpenseId/ AND group_id=$/groupId/`,
`WHERE recurring_expense_id=$/recurringExpenseId/ AND e.group_id=$/groupId/`,
`ORDER BY date ${first ? 'ASC' : 'DESC'} LIMIT 1`,
),
{ recurringExpenseId, userId, groupId },
Expand Down
4 changes: 2 additions & 2 deletions src/server/data/ExpenseSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function getExpenseSearchQuery(

return {
clause: expenseSelectClause(`--sql
WHERE group_id=$/groupId/
WHERE e.group_id=$/groupId/
AND template=false
AND ($/startDate/ IS NULL OR date::DATE >= $/startDate/::DATE)
AND ($/endDate/ IS NULL OR date::DATE <= $/endDate/::DATE)
Expand All @@ -39,7 +39,7 @@ export async function getExpenseSearchQuery(
${query.receiver ? `AND (receiver ILIKE '%$/receiver:value/%')` : ''}
AND (
$/search/ = ''
OR title ILIKE '%$/search:value/%'
OR e.title ILIKE '%$/search:value/%'
OR receiver ILIKE '%$/search:value/%'
)`),
params: {
Expand Down
2 changes: 1 addition & 1 deletion src/server/data/Expenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function getBetween(
);
const expenses = await tx.manyOrNone<UserExpense>(
expenseSelectClause(
`WHERE group_id=$/groupId/ AND template=false
`WHERE e.group_id=$/groupId/ AND template=false
AND date >= $/startDate/::DATE AND date < $/endDate/::DATE`,
),
{
Expand Down
18 changes: 10 additions & 8 deletions src/server/data/grouping/GroupingDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ const EXPENSE_SUM_SUBSELECT = /*sql*/ `
`;

const EXPENSE_JOIN_TO_GROUPING = /*sql*/ `
LEFT JOIN categories cat ON (cat.id = e.category_id)
LEFT JOIN expense_groupings eg ON (
eg.id = $/groupingId/
AND eg.id IN (SELECT expense_grouping_id FROM expense_grouping_categories egc WHERE egc.category_id IN (cat.id, cat.parent_id)
AND (eg.start_date IS NULL OR eg.start_date <= e.date)
AND (eg.end_date IS NULL OR eg.end_date >= e.date)
AND (eg.only_own IS FALSE or e.user_id=$/userId/)
))
WHERE e.group_id=$/groupId/
AND (
(e.grouping_id IS NULL AND eg.id = $/groupingId/)
Expand Down Expand Up @@ -218,6 +210,16 @@ export async function getCategoryTotalsForGrouping(
const rows = await tx.manyOrNone(
`SELECT SUM(CASE e.type WHEN 'expense' THEN sum WHEN 'income' THEN -sum ELSE 0 END), e.category_id, cat.name
FROM expenses e
LEFT JOIN categories cat ON (cat.id = e.category_id)
LEFT JOIN expense_groupings eg ON ((eg.id = e.grouping_id) OR (
eg.group_id = e.group_id
AND eg.id IN (
SELECT expense_grouping_id FROM expense_grouping_categories egc WHERE egc.category_id IN (cat.id, cat.parent_id)
AND (eg.start_date IS NULL OR eg.start_date <= e.date)
AND (eg.end_date IS NULL OR eg.end_date >= e.date)
AND (eg.only_own IS FALSE or e.user_id=$/userId/)
))
)
${EXPENSE_JOIN_TO_GROUPING}
GROUP BY e.category_id, cat.name;
`,
Expand Down
1 change: 1 addition & 0 deletions src/shared/expense/Expense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface ExpenseInputWithDefaults extends ExpenseInput {
}

export const UserExpense = Expense.extend({
autoGroupingIds: z.array(ObjectId),
userBalance: MoneyLike,
userBenefit: MoneyLike,
userCost: MoneyLike,
Expand Down
6 changes: 3 additions & 3 deletions src/shared/util/Arrays.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyObject } from '../types/Common';
import { AnyObject, isDefined } from '../types/Common';
import { BkError } from '../types/Errors';
import { getRandomInt } from './Util';

Expand Down Expand Up @@ -67,8 +67,8 @@ export function unnest<T>(arr: T[][]): T[] {
return res;
}

export function toArray<T>(t: T | T[]): T[] {
return Array.isArray(t) ? t : [t];
export function toArray<T>(t: T | T[] | undefined | null): T[] {
return isDefined(t) ? (Array.isArray(t) ? t : [t]) : [];
}

/** Assume input: Array of [name, value] fields */
Expand Down

0 comments on commit 6e2effd

Please sign in to comment.