Skip to content

Commit

Permalink
Add private and onlyOwn flags to expense groupings
Browse files Browse the repository at this point in the history
  • Loading branch information
thaapasa committed Mar 2, 2024
1 parent c361f61 commit 70281c4
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 22 deletions.
21 changes: 21 additions & 0 deletions migrations/20240302195554_add_flags_to_groupings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

/* eslint-disable no-undef */

exports.up = knex =>
knex.raw(/*sql*/ `
ALTER TABLE expense_groupings
ADD COLUMN user_id INTEGER REFERENCES users(id),
ADD COLUMN private BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN only_own BOOLEAN NOT NULL DEFAULT FALSE;
UPDATE expense_groupings eg SET user_id = (SELECT user_id FROM group_users gu WHERE gu.group_id = eg.group_id LIMIT 1);
ALTER TABLE expense_groupings ALTER COLUMN user_id SET NOT NULL;
`);

exports.down = knex =>
knex.raw(/*sql*/ `
ALTER TABLE expense_groupings
DROP COLUMN user_id,
DROP COLUMN private,
DROP COLUMN only_own;
`);
31 changes: 30 additions & 1 deletion src/client/ui/grouping/GroupingEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import styled from '@emotion/styled';
import { Button, Dialog, DialogContent, DialogTitle, Grid, IconButton } from '@mui/material';
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogTitle,
FormControlLabel,
Grid,
IconButton,
} from '@mui/material';
import * as B from 'baconjs';
import * as React from 'react';

Expand Down Expand Up @@ -85,6 +94,26 @@ const GroupingEditView: React.FC<{
<SelectionRow title="Nimi">
<TextEdit value={state.title} onChange={state.setTitle} fullWidth />
</SelectionRow>
<SelectionRow title="Valinnat">
<FormControlLabel
control={
<Checkbox
checked={state.private}
onChange={e => state.setPrivate(e.target.checked)}
/>
}
label="Yksityinen"
/>
<FormControlLabel
control={
<Checkbox
checked={state.onlyOwn}
onChange={e => state.setOnlyOwn(e.target.checked)}
/>
}
label="Vain omat kirjaukset"
/>
</SelectionRow>
<SelectionRow title="Alkupäivä">
<OptionalDatePicker value={state.startDate} onChange={state.setStartDate} />
</SelectionRow>
Expand Down
12 changes: 12 additions & 0 deletions src/client/ui/grouping/GroupingEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ export type GroupingState = {
tags: string[];
startDate: ISODate | null;
endDate: ISODate | null;
private: boolean;
onlyOwn: boolean;
reset(grouping: ExpenseGrouping | null): void;
setTitle(title: string): void;
setPrivate(isPrivate: boolean): void;
setOnlyOwn(onlyOwn: boolean): void;
setStartDate(date: ISODate | null): void;
setEndDate(date: ISODate | null): void;
setColor(color: string): void;
Expand All @@ -40,11 +44,15 @@ export const useGroupingState = create<GroupingState>((set, get) => ({
endDate: null,
color: colors.green[400],
categories: [],
private: false,
onlyOwn: false,
tags: [],
setTitle: title => set({ title }),
setColor: color => set({ color }),
setStartDate: startDate => set({ startDate }),
setEndDate: endDate => set({ endDate }),
setPrivate: isPrivate => set({ private: isPrivate }),
setOnlyOwn: onlyOwn => set({ onlyOwn }),
addTag: tag => {
const trimmed = tag?.trim();
if (trimmed) {
Expand All @@ -57,6 +65,8 @@ export const useGroupingState = create<GroupingState>((set, get) => ({
id: grouping?.id ?? null,
title: grouping?.title ?? '',
color: grouping?.color ?? '',
private: grouping?.private ?? false,
onlyOwn: grouping?.onlyOwn ?? false,
tags: [...(grouping?.tags ?? [])],
categories: grouping?.categories ?? [],
startDate: grouping?.startDate || null,
Expand All @@ -76,6 +86,8 @@ export const useGroupingState = create<GroupingState>((set, get) => ({
title: s.title,
color: s.color,
tags: s.tags,
private: s.private,
onlyOwn: s.onlyOwn,
categories: s.categories.length ? s.categories : [],
startDate: s.startDate ?? undefined,
endDate: s.endDate ?? undefined,
Expand Down
6 changes: 4 additions & 2 deletions src/server/api/GroupingApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ export function createGroupingApi() {
api.getTx('/tags', {}, (tx, session, {}) => getExpenseGroupingsTags(tx, session.group.id));

// GET /api/grouping/list
api.getTx('/list', {}, (tx, session, {}) => getExpenseGroupingsForUser(tx, session.group.id));
api.getTx('/list', {}, (tx, session, {}) =>
getExpenseGroupingsForUser(tx, session.group.id, session.user.id),
);

// GET /api/grouping/:id
api.getTx('/:id', {}, (tx, session, { params }) =>
getExpenseGrouping(tx, session.group.id, params.id),
getExpenseGrouping(tx, session.group.id, session.user.id, params.id),
);

// GET /api/grouping/:id/expenses
Expand Down
36 changes: 26 additions & 10 deletions src/server/data/grouping/GroupingDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const GROUPING_ORDER = /*sql*/ `eg.start_date DESC NULLS LAST, eg.title`;

const GROUPING_FIELDS = /*sql*/ `eg.id, eg.title,
eg.start_date AS "startDate", eg.end_date AS "endDate",
eg.created, eg.updated, eg.image, eg.color, eg.tags,
eg.created, eg.updated, eg.image, eg.color, eg.tags, eg.private, eg.only_own as "onlyOwn",
ARRAY_AGG(egc.category_id) AS categories`;

const EXPENSE_SUM_SUBSELECT = /*sql*/ `
Expand All @@ -32,6 +32,7 @@ const EXPENSE_SUM_SUBSELECT = /*sql*/ `
AND (cat.id = ANY(data.categories) OR cat.parent_id = ANY(data.categories))
AND (data."startDate" IS NULL OR e.date >= data."startDate")
AND (data."endDate" IS NULL OR e.date <= data."endDate")
AND (data."onlyOwn" IS FALSE OR e.user_id = $/userId/)
)
`;

Expand All @@ -42,6 +43,7 @@ const EXPENSE_JOIN_TO_GROUPING = /*sql*/ `
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 (
Expand All @@ -53,18 +55,19 @@ const EXPENSE_JOIN_TO_GROUPING = /*sql*/ `
export async function getExpenseGroupingsForUser(
tx: ITask<any>,
groupId: ObjectId,
userId: ObjectId,
): Promise<ExpenseGrouping[]> {
const rows = await tx.manyOrNone(
`SELECT data.*, (${EXPENSE_SUM_SUBSELECT}) AS "totalSum" FROM (
SELECT ${GROUPING_FIELDS}
FROM expense_groupings eg
LEFT JOIN expense_grouping_categories egc ON (eg.id = egc.expense_grouping_id)
WHERE eg.group_id=$/groupId/
WHERE eg.group_id=$/groupId/ AND (eg.private IS FALSE OR eg.user_id = $/userId/)
GROUP BY eg.id
ORDER BY ${GROUPING_ORDER}
) data
`,
{ groupId },
{ groupId, userId },
);
return rows.map(toExpenseGrouping);
}
Expand All @@ -87,19 +90,20 @@ export async function getExpenseGroupingsTags(
export async function getExpenseGroupingById(
tx: ITask<any>,
groupId: ObjectId,
userId: ObjectId,
groupingId: ObjectId,
): Promise<ExpenseGrouping | undefined> {
const row = await tx.oneOrNone(
`SELECT data.*, (${EXPENSE_SUM_SUBSELECT}) AS "totalSum" FROM (
SELECT ${GROUPING_FIELDS}
FROM expense_groupings eg
LEFT JOIN expense_grouping_categories egc ON (eg.id = egc.expense_grouping_id)
WHERE eg.id=$/groupingId/ AND group_id=$/groupId/
WHERE eg.id=$/groupingId/ AND group_id=$/groupId/ AND (eg.private IS FALSE OR eg.user_id = $/userId/)
GROUP BY eg.id
ORDER BY ${GROUPING_ORDER}
) data
`,
{ groupingId, groupId },
{ groupingId, groupId, userId },
);
return row ? toExpenseGrouping(row) : undefined;
}
Expand All @@ -109,7 +113,7 @@ export async function getAllGroupingRefs(
groupId: ObjectId,
): Promise<ExpenseGroupingRef[]> {
const rows = await tx.manyOrNone(
`SELECT eg.id, eg.title, eg.image, eg.color, eg.tags
`SELECT eg.id, eg.title, eg.image, eg.color, eg.tags, eg.private, eg.only_own AS "onlyOwn"
FROM expense_groupings eg
WHERE eg.group_id=$/groupId/
GROUP BY eg.id
Expand All @@ -123,20 +127,24 @@ export async function getAllGroupingRefs(
export async function insertExpenseGrouping(
tx: ITask<any>,
groupId: ObjectId,
userId: ObjectId,
data: ExpenseGroupingData,
): Promise<ObjectId> {
const row = await tx.one(
`INSERT INTO expense_groupings
(group_id, title, start_date, end_date, color, tags)
VALUES ($/groupId/, $/title/, $/startDate/, $/endDate/, $/color/, $/tags/::TEXT[])
(group_id, user_id, title, start_date, end_date, color, tags, private, only_own)
VALUES ($/groupId/, $/userId/, $/title/, $/startDate/, $/endDate/, $/color/, $/tags/::TEXT[], $/private/, $/onlyOwn/)
RETURNING id`,
{
groupId,
userId,
title: data.title,
startDate: data.startDate,
endDate: data.endDate,
color: data.color ?? '',
tags: data.tags ?? [],
private: data.private,
onlyOwn: data.onlyOwn,
},
);
const id = row.id;
Expand All @@ -159,7 +167,8 @@ export async function updateExpenseGroupingById(
): Promise<void> {
await tx.none(
`UPDATE expense_groupings
SET title=$/title/, start_date=$/startDate/, end_date=$/endDate/, color=$/color/, tags=$/tags/, updated=NOW()
SET title=$/title/, start_date=$/startDate/, end_date=$/endDate/, color=$/color/,
tags=$/tags/, private=$/private/, only_own=$/onlyOwn/, updated=NOW()
WHERE id=$/groupingId/`,
{
groupingId,
Expand All @@ -168,6 +177,8 @@ export async function updateExpenseGroupingById(
endDate: data.endDate,
color: data.color ?? '',
tags: data.tags ?? [],
private: data.private,
onlyOwn: data.onlyOwn,
},
);
await tx.none(`DELETE FROM expense_grouping_categories WHERE expense_grouping_id=$/groupingId/`, {
Expand Down Expand Up @@ -201,6 +212,7 @@ export async function getExpensesForGrouping(
export async function getCategoryTotalsForGrouping(
tx: ITask<any>,
groupId: ObjectId,
userId: ObjectId,
groupingId: ObjectId,
): Promise<ExpenseGroupingCategoryTotal[]> {
const rows = await tx.manyOrNone(
Expand All @@ -209,7 +221,7 @@ export async function getCategoryTotalsForGrouping(
${EXPENSE_JOIN_TO_GROUPING}
GROUP BY e.category_id, cat.name;
`,
{ groupId, groupingId },
{ groupId, userId, groupingId },
);
return rows.map(toExpenseGroupingCategoryTotal);
}
Expand Down Expand Up @@ -253,6 +265,8 @@ function toExpenseGrouping(row: any): ExpenseGrouping {
title: row.title,
color: row.color,
tags: row.tags,
private: row.private,
onlyOwn: row.onlyOwn,
categories: (row.categories ?? []).filter(isDefined),
startDate: row.startDate ? toISODate(row.startDate) : undefined,
endDate: row.endDate ? toISODate(row.endDate) : undefined,
Expand All @@ -268,6 +282,8 @@ function toExpenseGroupingRef(row: any): ExpenseGroupingRef {
color: row.color,
tags: row.tags,
image: row.image ? groupingImageHandler.getVariant('image', row.image).webPath : undefined,
private: row.private,
onlyOwn: row.onlyOwn,
};
}

Expand Down
19 changes: 10 additions & 9 deletions src/server/data/grouping/GroupingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ import {
export async function getExpenseGrouping(
tx: ITask<any>,
groupId: ObjectId,
userId: ObjectId,
groupingId: ObjectId,
): Promise<ExpenseGrouping> {
const grouping = await getExpenseGroupingById(tx, groupId, groupingId);
const grouping = await getExpenseGroupingById(tx, groupId, userId, groupingId);
if (!grouping) {
throw new NotFoundError('EXPENSE_GROUPING_NOT_FOUND', 'expense grouping', groupingId);
}
Expand All @@ -40,7 +41,7 @@ export async function createExpenseGrouping(
userId: ObjectId,
input: ExpenseGroupingData,
) {
const created = await insertExpenseGrouping(tx, groupId, input);
const created = await insertExpenseGrouping(tx, groupId, userId, input);
logger.info({ input, created }, `Created new expense grouping for user ${userId}`);
}

Expand All @@ -51,7 +52,7 @@ export async function updateExpenseGrouping(
groupingId: ObjectId,
input: ExpenseGroupingData,
) {
await getExpenseGrouping(tx, groupId, groupingId);
await getExpenseGrouping(tx, groupId, userId, groupingId);
const updated = await updateExpenseGroupingById(tx, groupingId, input);
logger.info({ input, updated }, `Updated expense grouping ${groupingId} for user ${userId}`);
}
Expand All @@ -62,7 +63,7 @@ export async function deleteExpenseGrouping(
userId: ObjectId,
groupingId: ObjectId,
) {
const grouping = await getExpenseGrouping(tx, groupId, groupingId);
const grouping = await getExpenseGrouping(tx, groupId, userId, groupingId);
await deleteExpenseGroupingById(tx, groupingId);
if (grouping.image) {
await groupingImageHandler.deleteImages(grouping.image);
Expand All @@ -78,15 +79,15 @@ export async function uploadExpenseGroupingImage(
image: FileUploadResult,
) {
try {
await getExpenseGrouping(tx, groupId, groupingId);
await getExpenseGrouping(tx, groupId, userId, groupingId);
logger.info(
image,
`Updating expense grouping image for user ${userId}, grouping ${groupingId}`,
);
const file = await groupingImageHandler.saveImages(image, { fit: 'cover' });
await deleteExpenseGroupingImage(tx, groupId, userId, groupingId);
await setGroupingImageById(tx, groupingId, file);
return getExpenseGrouping(tx, groupId, groupingId);
return getExpenseGrouping(tx, groupId, userId, groupingId);
} finally {
// Clear uploaded image
await safeDeleteFile(image.filepath);
Expand All @@ -99,7 +100,7 @@ export async function deleteExpenseGroupingImage(
userId: ObjectId,
groupingId: ObjectId,
): Promise<void> {
const grouping = await getExpenseGrouping(tx, groupId, groupingId);
const grouping = await getExpenseGrouping(tx, groupId, userId, groupingId);
if (!grouping.image) {
logger.info(`No image for expense grouping ${groupingId}, skipping delete...`);
return;
Expand All @@ -115,8 +116,8 @@ export async function getGroupingWithExpenses(
userId: ObjectId,
groupingId: ObjectId,
): Promise<ExpenseGroupingWithExpenses> {
const grouping = await getExpenseGrouping(tx, groupId, groupingId);
const grouping = await getExpenseGrouping(tx, groupId, userId, groupingId);
const expenses = await getExpensesForGrouping(tx, groupId, userId, groupingId);
const categoryTotals = await getCategoryTotalsForGrouping(tx, groupId, groupingId);
const categoryTotals = await getCategoryTotalsForGrouping(tx, groupId, userId, groupingId);
return { ...grouping, expenses, categoryTotals };
}
4 changes: 4 additions & 0 deletions src/shared/types/Grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const ExpenseGroupingData = z.object({
title: z.string(),
color: z.string(),
tags: z.array(z.string()),
private: z.boolean(),
onlyOwn: z.boolean(),
startDate: ISODate.optional(),
endDate: ISODate.optional(),
categories: z.array(ObjectId),
Expand All @@ -35,6 +37,8 @@ export const ExpenseGroupingRef = ExpenseGrouping.pick({
image: true,
color: true,
tags: true,
private: true,
onlyOwn: true,
});
export type ExpenseGroupingRef = z.infer<typeof ExpenseGroupingRef>;

Expand Down

0 comments on commit 70281c4

Please sign in to comment.