Skip to content

Commit

Permalink
Allow individual expenses to be marked in a grouping manually
Browse files Browse the repository at this point in the history
  • Loading branch information
thaapasa committed Feb 24, 2024
1 parent da74fa5 commit 46410ee
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 15 deletions.
4 changes: 3 additions & 1 deletion migrations/20240224123127_add_expense_groupings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ exports.up = knex =>
title TEXT NOT NULL,
start_date DATE,
end_date DATE,
sort_order INTEGER NOT NULL DEFAULT 0,
image TEXT
);
Expand All @@ -22,10 +21,13 @@ exports.up = knex =>
expense_grouping_id INTEGER REFERENCES expense_groupings(id) ON DELETE CASCADE,
category_id INTEGER REFERENCES categories(id) ON DELETE CASCADE
);
ALTER TABLE expenses ADD COLUMN grouping_id INTEGER REFERENCES expense_groupings(id) ON DELETE SET NULL;
`);

exports.down = knex =>
knex.raw(/*sql*/ `
ALTER TABLE expenses DROP COLUMN grouping_id;
DROP TABLE expense_grouping_categories;
DROP TABLE expense_groupings;
`);
16 changes: 15 additions & 1 deletion src/client/ui/expense/dialog/ExpenseDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
UserExpenseWithDetails,
} from 'shared/expense';
import { toDayjs, toISODate } from 'shared/time';
import { Category, CategoryMap, Group, Source, User } from 'shared/types';
import { Category, CategoryMap, ExpenseGrouping, Group, Source, User } from 'shared/types';
import { identity, Money, omit, sanitizeMoneyInput, valuesToArray } from 'shared/util';
import { CategoryDataSource, isSubcategoryOf } from 'client/data/Categories';
import { logger } from 'client/Logger';
Expand All @@ -39,6 +39,7 @@ import { DateField } from './DateField';
import {
DescriptionField,
ExpenseDialogContent,
GroupingSelector,
SourceSelector,
SumField,
TypeSelector,
Expand Down Expand Up @@ -80,6 +81,7 @@ const fields: ReadonlyArray<keyof ExpenseInEditor> = [
'confirmed',
'type',
'userId',
'groupingId',
];

const parsers: Record<string, (v: string) => any> = {
Expand Down Expand Up @@ -110,6 +112,7 @@ export interface ExpenseDialogProps<D> {
sourceMap: Record<string, Source>;
categorySource: CategoryDataSource[];
categoryMap: CategoryMap;
groupings: ExpenseGrouping[];
saveAction: ExpenseSaveAction | null;
onClose: (e: D | null) => void;
onExpensesUpdated: (date: Dayjs) => void;
Expand Down Expand Up @@ -194,6 +197,7 @@ export class ExpenseDialog extends React.Component<
confirmed: values.confirmed !== undefined ? values.confirmed : e ? e.confirmed : true,
type: values.type || (e ? e.type : 'expense'),
subcategories: [],
groupingId: e?.groupingId ?? null,
errors: {},
valid: false,
showOwnerSelect: false,
Expand Down Expand Up @@ -309,6 +313,7 @@ export class ExpenseDialog extends React.Component<
division,
date: toISODate(expense.date),
categoryId: expense.subcategoryId ? expense.subcategoryId : expense.categoryId,
groupingId: expense.groupingId ?? undefined,
};

this.saveLock.push(true);
Expand Down Expand Up @@ -494,6 +499,15 @@ export class ExpenseDialog extends React.Component<
errorText={this.state.errors.description}
/>
</Row>
<Row className="row select grouping-id">
<GroupingSelector
value={this.state.groupingId}
groupings={this.props.groupings}
style={{ flexGrow: 1 }}
title="Ryhmittely"
onChange={v => this.inputStreams.groupingId.push(v)}
/>
</Row>
</Form>
</ExpenseDialogContent>
<DialogActions>
Expand Down
33 changes: 32 additions & 1 deletion src/client/ui/expense/dialog/ExpenseDialogComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import * as React from 'react';

import { ExpenseType, expenseTypes, getExpenseTypeLabel } from 'shared/expense';
import { Source } from 'shared/types';
import { ExpenseGroupingRef, Source } from 'shared/types';
import { Money, sanitizeMoneyInput } from 'shared/util';
import { TextEdit } from 'client/ui/component/TextEdit';
import { ExpenseTypeIcon } from 'client/ui/icons/ExpenseType';
Expand Down Expand Up @@ -80,6 +80,37 @@ export const SourceSelector: React.FC<{
);
};

export const GroupingSelector: React.FC<{
value: number | null;
onChange: (id: number | null) => void;
groupings: ExpenseGroupingRef[];
style?: React.CSSProperties;
title: string;
}> = ({ title, value, style, onChange, groupings }) => {
const id = 'expense-dialog-grouping';
return (
<FormControl fullWidth={true} variant="standard">
<InputLabel htmlFor={id} shrink={true}>
{title}
</InputLabel>
<Select
labelId={id}
value={value ?? 0}
style={style}
label={title}
onChange={e => onChange(e.target.value ? Number(e.target.value) : null)}
>
<MenuItem value={0}>Oletus</MenuItem>
{groupings.map(s => (
<MenuItem key={s.id} value={s.id}>
{s.title}
</MenuItem>
))}
</Select>
</FormControl>
);
};

export const TypeSelector: React.FC<{
value: ExpenseType;
onChange: (s: ExpenseType) => void;
Expand Down
1 change: 1 addition & 0 deletions src/client/ui/expense/dialog/ExpenseDialogListener.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function createExpenseDialogListener<D>(
sourceMap: sourceMapE,
categorySource: categoryDataSourceP,
categoryMap: categoryMapE,
groupings: validSessionE.map(s => s.groupings),
users: validSessionE.map(s => s.users),
}),
)(Dialog);
Expand Down
1 change: 1 addition & 0 deletions src/client/ui/expense/dialog/NewExpenseDialogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const ConnectedExpenseDialog = connect(
sourceMap: sourceMapE,
categorySource: categoryDataSourceP,
categoryMap: categoryMapE,
groupings: validSessionE.map(s => s.groupings),
users: validSessionE.map(s => s.users),
}),
)(ExpenseDialog);
Expand Down
12 changes: 8 additions & 4 deletions src/server/data/BasicExpenseDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ 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(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.id, e.date::DATE, e.receiver, e.type, e.sum, e.title, e.description, e.confirmed, e.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 Down Expand Up @@ -79,6 +80,7 @@ export function dbRowToExpense(e: UserExpense): UserExpense {
}
e.date = toISODate(e.date);
e.userBalance = Money.from(e.userValue).negate().toString();
e.groupingId = e.groupingId ?? undefined;
return e;
}

Expand Down Expand Up @@ -224,11 +226,11 @@ export async function createNewExpense(
await tx.one<{ id: number }>(
`INSERT INTO expenses (
created_by_id, user_id, group_id, date, created, type,
receiver, sum, title, description, confirmed,
receiver, sum, title, description, confirmed, grouping_id,
source_id, category_id, template, recurring_expense_id)
VALUES (
$/userId/::INTEGER, $/expenseOwnerId/::INTEGER, $/groupId/::INTEGER, $/date/::DATE, NOW(), $/type/::expense_type,
$/receiver/, $/sum/, $/title/, $/description/, $/confirmed/::BOOLEAN,
$/receiver/, $/sum/, $/title/, $/description/, $/confirmed/::BOOLEAN, $/groupingId/,
$/sourceId/::INTEGER, $/categoryId/::INTEGER, $/template/::BOOLEAN, $/recurringExpenseId/)
RETURNING id`,
{
Expand All @@ -238,6 +240,7 @@ export async function createNewExpense(
sum: expense.sum.toString(),
template: expense.template || false,
recurringExpenseId: expense.recurringExpenseId || null,
groupingId: expense.groupingId,
},
)
).id;
Expand All @@ -262,12 +265,13 @@ export async function updateExpense(
await deleteDivision(tx, original.id);
await tx.none(
`UPDATE expenses
SET date=$/date/::DATE, receiver=$/receiver/, sum=$/sum/, title=$/title/,
SET date=$/date/::DATE, receiver=$/receiver/, sum=$/sum/, title=$/title/, grouping_id=$/groupingId/,
description=$/description/, type=$/type/::expense_type, confirmed=$/confirmed/::BOOLEAN,
source_id=$/sourceId/::INTEGER, category_id=$/categoryId/::INTEGER, user_id=$/userId/::INTEGER
WHERE id=$/id/`,
{
...expense,
groupingId: expense.groupingId,
id: original.id,
sum: expense.sum.toString(),
sourceId: source.id,
Expand Down
1 change: 1 addition & 0 deletions src/server/data/BasicExpenseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function createExpense(
sourceId: source.id,
categoryId: cat.id,
sum: expense.sum,
groupingId: expense.groupingId,
},
division,
);
Expand Down
6 changes: 4 additions & 2 deletions src/server/data/SessionDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { logger } from 'server/Logger';

import { config } from '../Config';
import { getAllCategories } from './CategoryDb';
import { getAllGroupingRefs } from './grouping/GroupingDb';
import { getShortcutsForUser } from './ShortcutDb';
import { getAllSources } from './SourceDb';
import {
Expand Down Expand Up @@ -93,13 +94,14 @@ export async function appendInfoToSession(
tx: ITask<any>,
session: SessionBasicInfo,
): Promise<Session> {
const [groups, sources, categories, users] = await Promise.all([
const [groups, sources, categories, users, groupings] = await Promise.all([
getGroupsForUser(tx, session.user.id),
getAllSources(tx, session.group.id),
getAllCategories(tx, session.group.id),
getAllUsers(tx, session.group.id),
getAllGroupingRefs(tx, session.group.id),
]);
return { ...session, groups, sources, categories, users };
return { ...session, groups, sources, categories, users, groupings };
}

export async function loginUserWithCredentials(
Expand Down
46 changes: 40 additions & 6 deletions src/server/data/grouping/GroupingDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { ITask } from 'pg-promise';

import { UserExpense } from 'shared/expense';
import { toISODate } from 'shared/time';
import { ExpenseGrouping, ExpenseGroupingData, isDefined, ObjectId } from 'shared/types';
import {
ExpenseGrouping,
ExpenseGroupingData,
ExpenseGroupingRef,
isDefined,
ObjectId,
} from 'shared/types';
import { groupingImageHandler } from 'server/content/GroupingImage';

import { dbRowToExpense, expenseSelectClause } from '../BasicExpenseDb';
Expand All @@ -23,7 +29,7 @@ export async function getExpenseGroupingsForUser(
LEFT JOIN expense_grouping_categories egc ON (eg.id = egc.expense_grouping_id)
WHERE eg.group_id=$/groupId/
GROUP BY eg.id
ORDER BY eg.sort_order
ORDER BY eg.start_date, eg.title
`,
{ groupId },
);
Expand All @@ -41,13 +47,29 @@ export async function getExpenseGroupingById(
LEFT JOIN expense_grouping_categories egc ON (eg.id = egc.expense_grouping_id)
WHERE eg.id=$/groupingId/ AND group_id=$/groupId/
GROUP BY eg.id
ORDER BY eg.sort_order
ORDER BY eg.start_date, eg.title
`,
{ groupingId, groupId },
);
return row ? toExpenseGrouping(row) : undefined;
}

export async function getAllGroupingRefs(
tx: ITask<any>,
groupId: ObjectId,
): Promise<ExpenseGroupingRef[]> {
const rows = await tx.manyOrNone(
`SELECT eg.id, eg.title, eg.image
FROM expense_groupings eg
WHERE eg.group_id=$/groupId/
GROUP BY eg.id
ORDER BY eg.start_date, eg.title
`,
{ groupId },
);
return rows.map(toExpenseGroupingRef);
}

export async function insertExpenseGrouping(
tx: ITask<any>,
groupId: ObjectId,
Expand Down Expand Up @@ -111,9 +133,13 @@ export async function getExpensesForGrouping(
LEFT JOIN expense_groupings eg ON (eg.id = egc.expense_grouping_id)
WHERE e.group_id=$/groupId/
AND (
egc.expense_grouping_id = $/groupingId/
AND (eg.start_date IS NULL OR eg.start_date <= e.date)
AND (eg.end_date IS NULL OR eg.end_date >= e.date)
(
e.grouping_id IS NULL
AND egc.expense_grouping_id = $/groupingId/
AND (eg.start_date IS NULL OR eg.start_date <= e.date)
AND (eg.end_date IS NULL OR eg.end_date >= e.date)
)
OR (e.grouping_id = $/groupingId/)
)
`),
{ userId, groupId, groupingId },
Expand Down Expand Up @@ -164,3 +190,11 @@ function toExpenseGrouping(row: any): ExpenseGrouping {
image: row.image ? groupingImageHandler.getVariant('image', row.image).webPath : undefined,
};
}

function toExpenseGroupingRef(row: any): ExpenseGroupingRef {
return {
id: row.id,
title: row.title,
image: row.image ? groupingImageHandler.getVariant('image', row.image).webPath : undefined,
};
}
2 changes: 2 additions & 0 deletions src/shared/expense/Expense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const ExpenseData = BaseExpenseData.extend({
date: ISODate,
sum: MoneyLike,
division: ExpenseDivision.optional(),
groupingId: ObjectId.optional(),
});
export type ExpenseData = z.infer<typeof ExpenseData>;

Expand Down Expand Up @@ -112,6 +113,7 @@ export interface ExpenseInEditor extends BaseExpenseData {
date: Dayjs;
benefit: number[];
description: string;
groupingId: number | null;
}

export interface UserExpenseWithDetails extends UserExpense {
Expand Down
3 changes: 3 additions & 0 deletions src/shared/types/Grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const ExpenseGrouping = ExpenseGroupingData.extend({
});
export type ExpenseGrouping = z.infer<typeof ExpenseGrouping>;

export const ExpenseGroupingRef = ExpenseGrouping.pick({ id: true, title: true, image: true });
export type ExpenseGroupingRef = z.infer<typeof ExpenseGroupingRef>;

export const ExpenseGroupingWithExpenses = ExpenseGrouping.extend({
expenses: z.array(UserExpense),
});
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/Session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ExpenseShortcut } from '../expense/Shortcut';
import { MoneyLike } from '../util/Money';
import { DbObject } from './Common';
import { ExpenseGroupingRef } from './Grouping';
import { Source } from './Source';

export interface Group extends DbObject {
Expand Down Expand Up @@ -53,4 +54,5 @@ export interface Session extends SessionBasicInfo {
sources: Source[];
categories: Category[];
users: User[];
groupings: ExpenseGroupingRef[];
}

0 comments on commit 46410ee

Please sign in to comment.