mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
Cleanup
This commit is contained in:
@@ -46,6 +46,8 @@ import {
|
||||
CellValue,
|
||||
CellValueText,
|
||||
} from '@desktop-client/components/spreadsheet/CellValue';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useCategoryMutations } from '@desktop-client/hooks/useCategoryMutations';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
@@ -144,32 +146,28 @@ export type ColumnDefinition = {
|
||||
};
|
||||
|
||||
type BudgetCategoriesProps = {
|
||||
categoryGroups: CategoryGroupEntity[];
|
||||
onBudgetAction: (month: string, type: string, args: unknown) => void;
|
||||
onShowActivity: (id: CategoryEntity['id'], month?: string) => void;
|
||||
onSaveCategory?: (category: CategoryEntity) => void;
|
||||
onSaveGroup?: (group: CategoryGroupEntity) => void;
|
||||
onDeleteCategory?: (id: CategoryEntity['id']) => void;
|
||||
onDeleteGroup?: (id: CategoryGroupEntity['id']) => void;
|
||||
onApplyBudgetTemplatesInGroup?: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onApplyBudgetTemplatesInGroup: (categoryIds: CategoryEntity['id'][]) => void;
|
||||
onToggleHiddenCategories: () => void;
|
||||
onCollapseAllCategories: () => void;
|
||||
onExpandAllCategories: () => void;
|
||||
};
|
||||
|
||||
export function BudgetCategories({
|
||||
categoryGroups,
|
||||
onBudgetAction,
|
||||
onShowActivity,
|
||||
onSaveCategory,
|
||||
onSaveGroup,
|
||||
onDeleteCategory,
|
||||
onDeleteGroup,
|
||||
onApplyBudgetTemplatesInGroup,
|
||||
onToggleHiddenCategories,
|
||||
onCollapseAllCategories,
|
||||
onExpandAllCategories,
|
||||
}: BudgetCategoriesProps) {
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
const {
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onShowActivity,
|
||||
} = useCategoryMutations();
|
||||
const { t } = useTranslation();
|
||||
const [collapsedGroupIds = [], setCollapsedGroupIdsPref] =
|
||||
useLocalPref('budget.collapsed');
|
||||
@@ -197,13 +195,13 @@ export function BudgetCategories({
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupCategories = expenseGroup.categories ?? [];
|
||||
|
||||
const expenseGroupCategories = collapsedGroupIds.includes(
|
||||
expenseGroup.id,
|
||||
)
|
||||
? []
|
||||
: expenseGroup.categories.filter(
|
||||
cat => showHiddenCategories || !cat.hidden,
|
||||
);
|
||||
: groupCategories.filter(cat => showHiddenCategories || !cat.hidden);
|
||||
|
||||
const expenseGroupItems: Item[] = [
|
||||
{
|
||||
@@ -261,11 +259,11 @@ export function BudgetCategories({
|
||||
});
|
||||
}
|
||||
|
||||
const groupCategories = incomeGroup.categories ?? [];
|
||||
|
||||
const incomeGroupCategories = collapsedGroupIds.includes(incomeGroup.id)
|
||||
? []
|
||||
: incomeGroup.categories.filter(
|
||||
cat => showHiddenCategories || !cat.hidden,
|
||||
);
|
||||
: groupCategories.filter(cat => showHiddenCategories || !cat.hidden);
|
||||
|
||||
incomeGroupItems.push(
|
||||
...incomeGroupCategories.map(
|
||||
@@ -510,10 +508,11 @@ export function BudgetCategories({
|
||||
onToggleVisibilty={group => {
|
||||
onSaveGroup({
|
||||
...group,
|
||||
hidden: !item.value.hidden,
|
||||
hidden: !!item.value.hidden ? false : true,
|
||||
});
|
||||
}}
|
||||
onApplyBudgetTemplatesInGroup={group =>
|
||||
group.categories &&
|
||||
onApplyBudgetTemplatesInGroup(
|
||||
group.categories
|
||||
.filter(cat => !cat.hidden)
|
||||
@@ -578,6 +577,7 @@ export function BudgetCategories({
|
||||
});
|
||||
}}
|
||||
onApplyBudgetTemplatesInGroup={group =>
|
||||
group.categories &&
|
||||
onApplyBudgetTemplatesInGroup(
|
||||
group.categories
|
||||
.filter(cat => !cat.hidden)
|
||||
@@ -614,14 +614,15 @@ export function BudgetCategories({
|
||||
id="new-category-row"
|
||||
columns={columns}
|
||||
onUpdate={name => {
|
||||
if (name) {
|
||||
const group = categoryGroups.find(
|
||||
g => g.id === groupOfNewCategory,
|
||||
);
|
||||
if (name && group) {
|
||||
onSaveCategoryAndClose({
|
||||
id: 'new',
|
||||
name,
|
||||
group: groupOfNewCategory,
|
||||
is_income:
|
||||
groupOfNewCategory ===
|
||||
categoryGroups.find(g => g.is_income).id,
|
||||
group: group.id,
|
||||
is_income: group.is_income,
|
||||
});
|
||||
} else {
|
||||
onHideNewCategoryInput();
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
separateGroups,
|
||||
} from './util';
|
||||
|
||||
import { type BudgetComponents } from '.';
|
||||
|
||||
import { type DropPosition } from '@desktop-client/components/sort';
|
||||
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
@@ -308,13 +310,7 @@ export function BudgetTable(props: BudgetTableProps) {
|
||||
{budgetTableV2Enabled && (
|
||||
<View style={{ overflowY: 'auto' }}>
|
||||
<BudgetCategoriesV2
|
||||
categoryGroups={categoryGroups}
|
||||
onSaveCategory={onSaveCategory}
|
||||
onSaveGroup={onSaveGroup}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onDeleteGroup={onDeleteGroup}
|
||||
onBudgetAction={onBudgetAction}
|
||||
onShowActivity={onShowActivity}
|
||||
onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup}
|
||||
onToggleHiddenCategories={onToggleHiddenCategories}
|
||||
onExpandAllCategories={onExpandAllCategories}
|
||||
|
||||
@@ -86,11 +86,6 @@ export function CategoryBalanceCell({
|
||||
typeof categoryBudgetedBinding
|
||||
>(categoryBudgetedBinding);
|
||||
|
||||
const balanceValue = useSheetValue<
|
||||
typeof bindingBudgetType,
|
||||
typeof categoryBalanceBinding
|
||||
>(categoryBalanceBinding);
|
||||
|
||||
const goalValue = useSheetValue<
|
||||
typeof bindingBudgetType,
|
||||
typeof categoryGoalBinding
|
||||
@@ -103,10 +98,6 @@ export function CategoryBalanceCell({
|
||||
|
||||
const [isBalanceMenuOpen, setIsBalanceMenuOpen] = useState(false);
|
||||
|
||||
const [activeBalanceMenu, setActiveBalanceMenu] = useState<
|
||||
'balance' | 'transfer' | 'cover' | null
|
||||
>(null);
|
||||
|
||||
const { pressProps } = usePress({
|
||||
onPress: () => setIsBalanceMenuOpen(true),
|
||||
});
|
||||
|
||||
@@ -229,8 +229,10 @@ function BudgetedInput({
|
||||
onUpdate={(newValue, e) => {
|
||||
onUpdate?.(newValue, e);
|
||||
const integerAmount = currencyToInteger(newValue);
|
||||
onUpdateAmount?.(integerAmount);
|
||||
setCurrentFormattedAmount(format(integerAmount, 'financial'));
|
||||
if (integerAmount) {
|
||||
onUpdateAmount?.(integerAmount);
|
||||
setCurrentFormattedAmount(format(integerAmount, 'financial'));
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -143,10 +143,14 @@ export function CategoryGroupNameCell({
|
||||
items={[
|
||||
{ name: 'add-category', text: t('Add category') },
|
||||
{ name: 'rename', text: t('Rename') },
|
||||
!categoryGroup.is_income && {
|
||||
name: 'toggle-visibility',
|
||||
text: categoryGroup.hidden ? 'Show' : 'Hide',
|
||||
},
|
||||
...(!categoryGroup.is_income
|
||||
? [
|
||||
{
|
||||
name: 'toggle-visibility',
|
||||
text: categoryGroup.hidden ? 'Show' : 'Hide',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// onDelete && { name: 'delete', text: t('Delete') },
|
||||
{ name: 'delete', text: t('Delete') },
|
||||
...(isGoalTemplatesEnabled
|
||||
|
||||
@@ -112,10 +112,14 @@ export function CategoryNameCell({
|
||||
}}
|
||||
items={[
|
||||
{ name: 'rename', text: t('Rename') },
|
||||
!categoryGroup?.hidden && {
|
||||
name: 'toggle-visibility',
|
||||
text: category.hidden ? t('Show') : t('Hide'),
|
||||
},
|
||||
...(!categoryGroup?.hidden
|
||||
? [
|
||||
{
|
||||
name: 'toggle-visibility',
|
||||
text: category.hidden ? t('Show') : t('Hide'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ name: 'delete', text: t('Delete') },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -543,6 +543,13 @@ export function IncomeGroupMonth({ month }: IncomeGroupMonthProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type IncomeCategoryMonthProps = {
|
||||
category: CategoryEntity;
|
||||
isLast?: boolean;
|
||||
month: string;
|
||||
onShowActivity: (id: CategoryEntity['id'], month: string) => void;
|
||||
onBudgetAction: (month: string, action: string, arg?: unknown) => void;
|
||||
};
|
||||
export function IncomeCategoryMonth({
|
||||
category,
|
||||
isLast,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
getCategories,
|
||||
} from '@desktop-client/budget/budgetSlice';
|
||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||
import { useCategoryActions } from '@desktop-client/hooks/useCategoryActions';
|
||||
import { useCategoryMutations } from '@desktop-client/hooks/useCategoryMutations';
|
||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
|
||||
@@ -138,7 +138,7 @@ export function Budget() {
|
||||
onShowActivity,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
} = useCategoryActions();
|
||||
} = useCategoryMutations();
|
||||
|
||||
if (!initialized || !categoryGroups) {
|
||||
return null;
|
||||
|
||||
228
packages/desktop-client/src/hooks/useCategoryMutations.ts
Normal file
228
packages/desktop-client/src/hooks/useCategoryMutations.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import {
|
||||
type CategoryEntity,
|
||||
type CategoryGroupEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import { useCategories } from './useCategories';
|
||||
import { useNavigate } from './useNavigate';
|
||||
|
||||
import {
|
||||
createCategory,
|
||||
createCategoryGroup,
|
||||
deleteCategory,
|
||||
deleteCategoryGroup,
|
||||
moveCategory,
|
||||
moveCategoryGroup,
|
||||
updateCategory,
|
||||
updateCategoryGroup,
|
||||
} from '@desktop-client/budget/budgetSlice';
|
||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
export function useCategoryMutations() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { grouped: categoryGroups } = useCategories();
|
||||
|
||||
const categoryNameAlreadyExistsNotification = (
|
||||
name: CategoryEntity['name'],
|
||||
) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
notification: {
|
||||
type: 'error',
|
||||
message: t(
|
||||
'Category “{{name}}” already exists in group (it may be hidden)',
|
||||
{ name },
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveCategory = async (category: CategoryEntity) => {
|
||||
const { grouped: categoryGroups = [] } = await send('get-categories');
|
||||
|
||||
const group = categoryGroups.find(g => g.id === category.group);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
const exists =
|
||||
groupCategories
|
||||
.filter(c => c.name.toUpperCase() === category.name.toUpperCase())
|
||||
.filter(c => (category.id === 'new' ? true : c.id !== category.id))
|
||||
.length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(category.name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (category.id === 'new') {
|
||||
dispatch(
|
||||
createCategory({
|
||||
name: category.name,
|
||||
groupId: category.group,
|
||||
isIncome: !!category.is_income,
|
||||
isHidden: !!category.hidden,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(updateCategory({ category }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteCategory = async (id: CategoryEntity['id']) => {
|
||||
const mustTransfer = await send('must-category-transfer', { id });
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
category: id,
|
||||
onDelete: transferCategory => {
|
||||
if (id !== transferCategory) {
|
||||
dispatch(
|
||||
deleteCategory({ id, transferId: transferCategory }),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategory({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveGroup = (group: CategoryGroupEntity) => {
|
||||
if (group.id === 'new') {
|
||||
dispatch(createCategoryGroup({ name: group.name }));
|
||||
} else {
|
||||
dispatch(updateCategoryGroup({ group }));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteGroup = async (id: CategoryGroupEntity['id']) => {
|
||||
const group = categoryGroups.find(g => g.id === id);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
let mustTransfer = false;
|
||||
for (const category of groupCategories) {
|
||||
if (await send('must-category-transfer', { id: category.id })) {
|
||||
mustTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mustTransfer) {
|
||||
dispatch(
|
||||
pushModal({
|
||||
modal: {
|
||||
name: 'confirm-category-delete',
|
||||
options: {
|
||||
group: id,
|
||||
onDelete: transferCategory => {
|
||||
dispatch(
|
||||
deleteCategoryGroup({ id, transferId: transferCategory }),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(deleteCategoryGroup({ id }));
|
||||
}
|
||||
};
|
||||
|
||||
const onShowActivity = (categoryId: CategoryEntity['id'], month: string) => {
|
||||
const filterConditions = [
|
||||
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
|
||||
{
|
||||
field: 'date',
|
||||
op: 'is',
|
||||
value: month,
|
||||
options: { month: true },
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
navigate('/accounts', {
|
||||
state: {
|
||||
goBack: true,
|
||||
filterConditions,
|
||||
categoryId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onReorderCategory = async (sortInfo: {
|
||||
id: CategoryEntity['id'];
|
||||
groupId?: CategoryGroupEntity['id'];
|
||||
targetId: CategoryEntity['id'] | null;
|
||||
}) => {
|
||||
const { grouped: categoryGroups = [], list: categories = [] } =
|
||||
await send('get-categories');
|
||||
|
||||
const moveCandidate = categories.find(c => c.id === sortInfo.id);
|
||||
const group = categoryGroups.find(g => g.id === sortInfo.groupId);
|
||||
|
||||
if (!moveCandidate || !group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCategories = group.categories ?? [];
|
||||
|
||||
const exists =
|
||||
groupCategories
|
||||
.filter(c => c.name.toUpperCase() === moveCandidate.name.toUpperCase())
|
||||
.filter(c => c.id !== moveCandidate.id).length > 0;
|
||||
|
||||
if (exists) {
|
||||
categoryNameAlreadyExistsNotification(moveCandidate.name);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
moveCategory({
|
||||
id: moveCandidate.id,
|
||||
groupId: group.id,
|
||||
targetId: sortInfo.targetId,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onReorderGroup = async (sortInfo: {
|
||||
id: CategoryGroupEntity['id'];
|
||||
targetId: CategoryGroupEntity['id'] | null;
|
||||
}) => {
|
||||
dispatch(
|
||||
moveCategoryGroup({ id: sortInfo.id, targetId: sortInfo.targetId }),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
onSaveCategory,
|
||||
onDeleteCategory,
|
||||
onSaveGroup,
|
||||
onDeleteGroup,
|
||||
onShowActivity,
|
||||
onReorderCategory,
|
||||
onReorderGroup,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user