Skip to content

Commit

Permalink
Collapsed representation of sub-categories to simplify UX
Browse files Browse the repository at this point in the history
  • Loading branch information
ystxn committed Mar 9, 2024
1 parent a5357f0 commit de6839b
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 177 deletions.
13 changes: 9 additions & 4 deletions src/dashboard/insights.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ const Insights = () => {
average: summary[category].average,
}));
setCategorySummary(summaryList);
setInsights(response);
const processedResponse = { ...response };
processedResponse.summary = processedResponse.summary.map((o) => {
const subCat = o.subCategory && o.subCategory !== o.category ? `${o.category}: ${o.subCategory}` : o.category;
return { ...o, subCategory: subCat };
});
setInsights(processedResponse);
}), []);

const AverageGrid = () => {
Expand All @@ -96,9 +101,9 @@ const Insights = () => {
const filter = {
items: [
{
field: breakdown ? 'subCategory' : 'category',
field: 'category',
id: 1,
operator: value ? "equals" : "isEmpty",
operator: value ? (breakdown ? 'equals' : 'startsWith') : 'isEmpty',
value: value || undefined,
},
],
Expand Down Expand Up @@ -129,7 +134,7 @@ const Insights = () => {
disableColumnMenu
density="compact"
rows={breakdown ? insights.summary : categorySummary}
columns={breakdown ? columns : columns.filter(({ field }) => field !== 'subCategory')}
columns={columns.filter(({ field }) => field !== (breakdown ? 'category' : 'subCategory'))}
sx={maxGridSize}
getRowId={({ category, subCategory }) => category + subCategory }
initialState={{
Expand Down
73 changes: 29 additions & 44 deletions src/settings/templates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ const Templates = ({ isMobile }) => {
const apiRef = useGridApiRef();
const [ originalData, setOriginalData ] = state.useState(state.templates);
const [ categories, setCategories ] = state.useState(state.categories);
const [ categoryOptions, setCategoryOptions ] = useState([]);
const [ subCategoryOptions, setSubCategoryOptions ] = useState([]);
const [ categoryMap, setCategoryMap ] = useState();
const [ data, setData ] = useState();
const [ selectedRows, setSelectedRows ] = useState([]);
const [ showConfirmDelete, setShowConfirmDelete ] = useState(false);
Expand All @@ -51,12 +48,6 @@ const Templates = ({ isMobile }) => {

const handleChange = (e, v) => {
apiRef.current.setEditCellValue({ id, field, value: v?.label || v });
// TODO: Make universal endpoint
// const match = transactions.find((t) => t.remarks === v);
// if (match) {
// apiRef.current.setEditCellValue({ id, field: 'category', value: match.category });
// apiRef.current.setEditCellValue({ id, field: 'subCategory', value: match.subCategory });
// }
};

return (
Expand Down Expand Up @@ -92,32 +83,21 @@ const Templates = ({ isMobile }) => {
apiRef.current.setEditCellValue({ id, field, value: v?.label || v });
};

const handleInputChange = (e, v) => {
if (!props.sub) {
return;
}
const lookupCategory = categoryMap[v];
if (lookupCategory) {
apiRef.current.setEditCellValue({ id, field: 'category', value: lookupCategory });
}
};

return (
<Autocomplete
freeSolo
options={props.sub ? subCategoryOptions : categoryOptions}
options={categories}
filterOptions={createFilterOptions({ limit: 5 })}
value={value || ""}
onChange={handleChange}
onBlur={(e) => handleChange(e, e.target.value)}
onInputChange={handleInputChange}
disableClearable
sx={{ flex: 1 }}
renderInput={(params) => (
<TextField
inputRef={ref}
name={props.sub ? "subCategory" : "category"}
label={props.sub ? "Sub-category" : "Category"}
name="category"
label="Category"
{...params}
/>
)}
Expand All @@ -129,38 +109,39 @@ const Templates = ({ isMobile }) => {
{ editable: true, flex: 1, field: 'reference', headerName: 'Reference', },
{ editable: true, flex: 1, field: 'remarks', headerName: 'Remarks', renderEditCell: (p) => <RemarksEditor {...p} /> },
{ editable: true, flex: 1, field: 'category', headerName: 'Category', renderEditCell: (p) => <CategoryEditor {...p} /> },
{ editable: true, flex: 1, field: 'subCategory', headerName: 'Sub-category', renderEditCell: (p) => <CategoryEditor sub {...p} /> },
];

const prepareOptions = (array, field) => ([
...new Set(array.map((s) => s[field]))
].map((o) => ({ label: o })));

useEffect(() => {
if (!originalData) {
listTemplates((response) => setOriginalData(response));
listTemplates((response) => {
const processedResponse = response.map((r) => ({
...r,
category: r.subCategory && r.subCategory !== r.category
? `${r.category}: ${r.subCategory}`
: r.category,
}))
setOriginalData(processedResponse);
});
}
if (categories.length === 0) {
getCategories((response) => setCategories(response));
if (categories.length > 0) {
return;
}
getCategories((response) => {
const processed = response.map(({ category, subCategory}) =>
subCategory && subCategory !== category ? `${category}: ${subCategory}` : category);
setCategories(processed);
});
}, []);

useEffect(() => setData(originalData), [ originalData ]);

useEffect(() => {
setCategoryOptions(prepareOptions(categories, 'category'));
setSubCategoryOptions(prepareOptions(categories, 'subCategory'));
const tempMap = {};
categories.forEach(({ category, subCategory }) => {
if (!tempMap[subCategory]) {
tempMap[subCategory] = category;
}
});
setCategoryMap(tempMap);
}, [ categories ]);

const postProcess = (row, newRows, verb) => {
setOriginalData(data.map((o) => o.id !== row.id ? o : newRows[0]));
const newRow = newRows[0];
newRow.category = newRow.subCategory && newRow.subCategory !== newRow.category
? `${newRow.category}: ${newRow.subCategory}`
: newRow.category,

setOriginalData(data.map((o) => o.id !== row.id ? o : newRow));
showStatus('success', 'Template ' + verb);
};

Expand All @@ -179,6 +160,10 @@ const Templates = ({ isMobile }) => {
}

delete row.owner;
const parts = row.category.split(':');
row.category = parts.shift().trim();
row.subCategory = parts.join(':').trim() || row.category;

const existing = originalData.find(t => t.id === row.id);
if (existing) {
let match = true;
Expand Down
64 changes: 17 additions & 47 deletions src/transactions/add-transaction-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,38 +56,23 @@ const AddTransactionDialog = ({
const selectedAccount = !transactionToEdit ? state.useState(state.selectedAccount)[0]
: accounts.find(({ id }) => id === transactionToEdit.accountId);
const [ categories, setCategories ] = state.useState(state.categories);
const [ categoryOptions, setCategoryOptions ] = useState([]);
const [ subCategoryOptions, setSubCategoryOptions ] = useState([]);
const [ category, setCategory ] = useState('');
const [ subCategory, setSubCategory ] = useState('');
const [ categoryMap, setCategoryMap ] = useState({});
const setVisibleTransactionId = state.useState(state.visibleTransactionId)[1];
const {
listAccounts, addTransaction, editTransaction, listTransactions,
showStatus, suggestRemarks, suggestCode, suggestCompany, getCategories,
} = api();

useEffect(() => {
if (categories.length === 0) {
getCategories((response) => setCategories(response));
if (categories.length > 0) {
return;
}
}, []);

const prepareOptions = (array, field) => ([
...new Set(array.map((s) => s[field]))
].map((o) => ({ label: o })));

useEffect(() => {
setCategoryOptions(prepareOptions(categories, 'category'));
setSubCategoryOptions(prepareOptions(categories, 'subCategory'));
const tempMap = {};
categories.forEach(({ category, subCategory }) => {
if (!tempMap[subCategory]) {
tempMap[subCategory] = category;
}
getCategories((response) => {
const processed = response.map(({ category, subCategory}) =>
subCategory && subCategory !== category ? `${category}: ${subCategory}` : category);
setCategories(processed);
});
setCategoryMap(tempMap);
}, [ categories ]);
}, []);

useEffect(() => {
if (!transactionToEdit) {
Expand All @@ -100,8 +85,9 @@ const AddTransactionDialog = ({
} else if (selectedAccount?.type === 'Retirement') {
setMonth(dayjs.utc(transactionToEdit.forMonth));
}
setCategory(transactionToEdit.category || '');
setSubCategory(transactionToEdit.subCategory || '');
if (transactionToEdit.category) {
setCategory(transactionToEdit.category);
}
}, [ selectedAccount, transactionToEdit ]);

useEffect(() => {
Expand All @@ -121,6 +107,11 @@ const AddTransactionDialog = ({
accountId: selectedAccount?.id,
...Object.fromEntries(new FormData(event.target).entries()),
};

const parts = tx.category.split(':');
tx.category = parts.shift().trim();
tx.subCategory = parts.join(':').trim() || tx.category;

if (tx.remarks) {
tx.remarks = tx.remarks.trim();
}
Expand Down Expand Up @@ -242,8 +233,7 @@ const AddTransactionDialog = ({
}
const match = transactions.find((t) => t.remarks === value);
if (match) {
setCategory(match.category);
setSubCategory(match.subCategory);
setCategory(match.subCategory ? `${match.category}: ${match.subCategory}` : match.category);
}
};

Expand Down Expand Up @@ -349,7 +339,7 @@ const AddTransactionDialog = ({
<Autocomplete
key="category"
freeSolo
options={categoryOptions}
options={categories}
filterOptions={createFilterOptions({ limit: 5 })}
value={category}
onChange={(e, v) => setCategory(v)}
Expand All @@ -364,25 +354,6 @@ const AddTransactionDialog = ({
)}
/>
),
subCategory: (
<Autocomplete
key="subCategory"
freeSolo
defaultValue={transactionToEdit?.subCategory}
options={subCategoryOptions}
filterOptions={createFilterOptions({ limit: 5 })}
value={subCategory}
onChange={(e, v) => setSubCategory(v)}
onInputChange={(e, value) => setCategory((old) => categoryMap[value] || old)}
renderInput={(params) => (
<TextField
name="subCategory"
label="Sub-category"
{...params}
/>
)}
/>
),
code: (
<AutoFill
key="code"
Expand Down Expand Up @@ -449,7 +420,6 @@ const AddTransactionDialog = ({
fields.amount,
fields.remarks,
fields.category,
fields.subCategory,
];
const cpfFields = [
fields.date,
Expand Down
59 changes: 21 additions & 38 deletions src/transactions/bulk-transaction-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,49 +31,45 @@ const BulkTransactionDialog = ({
const [ transactions, setTransactions ] = state.useState(state.transactions);
const [ month, setMonth ] = useState();
const [ categories, setCategories ] = state.useState(state.categories);
const [ categoryOptions, setCategoryOptions ] = useState([]);
const [ subCategoryOptions, setSubCategoryOptions ] = useState([]);
const [ category, setCategory ] = useState('');
const [ categoryMap, setCategoryMap ] = useState();

const {
bulkEditTransactions, suggestRemarks, showStatus, getCategories,
} = api();

useEffect(() => {
if (categories.length === 0) {
getCategories((response) => setCategories(response));
if (categories.length > 0) {
return;
}
}, []);

const prepareOptions = (array, field) => ([
...new Set(array.map((s) => s[field]))
].map((o) => ({ label: o })));

useEffect(() => {
setCategoryOptions(prepareOptions(categories, 'category'));
setSubCategoryOptions(prepareOptions(categories, 'subCategory'));
const tempMap = {};
categories.forEach(({ category, subCategory }) => {
if (!tempMap[subCategory]) {
tempMap[subCategory] = category;
}
getCategories((response) => {
const processed = response.map(({ category, subCategory}) =>
subCategory && subCategory !== category ? `${category}: ${subCategory}` : category);
setCategories(processed);
});
setCategoryMap(tempMap);
}, [ categories ]);
}, []);

const submit = (event) => {
event.preventDefault();
const { category, subCategory, remarks } = Object.fromEntries(new FormData(event.target).entries());
const { category, remarks } = Object.fromEntries(new FormData(event.target).entries());

const parts = category.trim().length === 0 ? null : category.split(':');
const processedCategory = parts ? parts.shift().trim() : null;
const processedSubCategory = parts ? (parts.join(':').trim() || processedCategory) : null;

const values = {
ids: transactionToEdit,
category: category.trim().length === 0 ? null : category.trim(),
subCategory: subCategory.trim().length === 0 ? null : subCategory.trim(),
category: processedCategory,
subCategory: processedSubCategory,
remarks: remarks.trim().length === 0 ? null : remarks.trim(),
};
if (selectedAccount?.type === 'Credit') {
values.billingMonth = month?.utc().startOf('month').toISOString();
}

if (Object.values(values).filter(i => i).length === 1) {
showStatus('warning', 'Enter at least 1 value to edit');
return;
}
setLoading(true);
bulkEditTransactions(values, () => {
const revised = [ ...transactions ].map((t) =>
Expand Down Expand Up @@ -122,7 +118,7 @@ const BulkTransactionDialog = ({
/>
<Autocomplete
freeSolo
options={categoryOptions}
options={categories}
filterOptions={createFilterOptions({ limit: 5 })}
value={category}
onChange={(e, v) => setCategory(v)}
Expand All @@ -134,19 +130,6 @@ const BulkTransactionDialog = ({
/>
)}
/>
<Autocomplete
freeSolo
options={subCategoryOptions}
filterOptions={createFilterOptions({ limit: 5 })}
onInputChange={(e, value) => setCategory((old) => categoryMap[value] || old)}
renderInput={(params) => (
<TextField
name="subCategory"
label="Sub-category"
{...params}
/>
)}
/>
</LocalizationProvider>

<Stack direction="row" spacing={2}>
Expand Down
Loading

0 comments on commit de6839b

Please sign in to comment.