diff --git a/src/App.css b/src/App.css index 18c38aa..b0fdebe 100644 --- a/src/App.css +++ b/src/App.css @@ -115,6 +115,10 @@ input.text-end.form-control:focus { color: var(--textcolor); } +.form-control:disabled { + background: var(--lightbgcolor); +} + /* Chrome, Safari, Edge, Opera */ input::-webkit-inner-spin-button, input::-webkit-outer-spin-button { diff --git a/src/App.test.tsx b/src/App.test.tsx index e997c31..e937b95 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,7 +1,7 @@ import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import App from "./App"; -import { budgetsDB, optionsDB } from "./db"; +import { budgetsDB, calcHistDB, optionsDB } from "./db"; import { budgetContextSpy, testEmptyBudgetContext } from "./setupTests"; describe("App", () => { @@ -23,6 +23,8 @@ describe("App", () => { expect(budgetsDB.config("storeName")).toBe("budgets"); expect(optionsDB.config("name")).toBe("guitos"); expect(optionsDB.config("storeName")).toBe("options"); + expect(calcHistDB.config("name")).toBe("guitos"); + expect(calcHistDB.config("storeName")).toBe("calcHistDB"); }); it("shows new budget when clicking new button", async () => { diff --git a/src/components/CalculateButton/CalculateButton.test.tsx b/src/components/CalculateButton/CalculateButton.test.tsx index af0d909..0d07a13 100644 --- a/src/components/CalculateButton/CalculateButton.test.tsx +++ b/src/components/CalculateButton/CalculateButton.test.tsx @@ -155,4 +155,15 @@ describe("CalculateButton", () => { expect(onCalculate).toHaveBeenCalledWith(123, "divide"); }); + + it("shows history when clicking button", async () => { + const button = screen.getByRole("button", { + name: "select operation type to item value", + }); + await userEvent.click(button); + const historyButton = screen.getByRole("button", { + name: "open operation history", + }); + await userEvent.click(historyButton); + }); }); diff --git a/src/components/CalculateButton/CalculateButton.tsx b/src/components/CalculateButton/CalculateButton.tsx index 472d4b3..51cfd69 100644 --- a/src/components/CalculateButton/CalculateButton.tsx +++ b/src/components/CalculateButton/CalculateButton.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button, Dropdown, @@ -7,16 +7,39 @@ import { Popover, } from "react-bootstrap"; import CurrencyInput from "react-currency-input-field"; -import { BsCheckLg, BsDashLg, BsPlusSlashMinus, BsXLg } from "react-icons/bs"; +import { + BsCheckLg, + BsClockHistory, + BsDashLg, + BsPlusSlashMinus, + BsXLg, +} from "react-icons/bs"; import { CgMathDivide, CgMathPlus } from "react-icons/cg"; +import { useBudget } from "../../context/BudgetContext"; import { useConfig } from "../../context/ConfigContext"; +import { useDB } from "../../hooks/useDB"; import { ItemForm } from "../ItemForm/ItemForm"; import "./CalculateButton.css"; interface CalculateButtonProps { itemForm: ItemForm; label: string; - onCalculate: (changeValue: number, operation: string) => void; + onCalculate: (changeValue: number, operation: ItemOperation) => void; +} + +export type ItemOperation = + | "name" + | "value" + | "add" + | "subtract" + | "multiply" + | "divide"; + +export interface CalculationHistoryItem { + id: string; + itemForm: ItemForm; + changeValue: number | undefined; + operation: ItemOperation; } export function CalculateButton({ @@ -24,11 +47,16 @@ export function CalculateButton({ label, onCalculate, }: CalculateButtonProps) { - const [operation, setOperation] = useState("add"); + const [operation, setOperation] = useState("add"); const [changeValue, setChangeValue] = useState(0); + const [showHistory, setShowHistory] = useState(false); + const [history, setHistory] = useState([]); const opButtonRef = useRef(null); const inputRef = useRef(null); const { intlConfig } = useConfig(); + const { getCalcHist } = useDB(); + const { budget } = useBudget(); + const calcHistID = `${budget?.id}-${label}-${itemForm.id}`; function handleKeyPress(e: { key: string }) { if (e.key === "Enter") { @@ -43,9 +71,29 @@ export function CalculateButton({ function handleCalculate() { if (changeValue > 0) { onCalculate(changeValue, operation); + setShowHistory(false); + getHistory(); } } + function handleHistory() { + getHistory(); + setShowHistory(!showHistory); + } + + function getHistory() { + getCalcHist(calcHistID) + .then((h) => setHistory(h)) + .catch((e: unknown) => { + throw e; + }); + } + + useEffect(() => { + getHistory(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showHistory]); + return ( <> + + {showHistory && ( +
+ {history + .filter((i) => i.operation !== "value") + .map((item, index) => ( + + + + {item.operation === "add" && ( + + )} + {item.operation === "subtract" && ( + + )} + {item.operation === "multiply" && ( + + )} + {item.operation === "divide" && ( + + )} + + + + )) + .reverse()} +
+ )} } diff --git a/src/components/ItemForm/ItemFormGroup.tsx b/src/components/ItemForm/ItemFormGroup.tsx index db4fee2..9b148a8 100644 --- a/src/components/ItemForm/ItemFormGroup.tsx +++ b/src/components/ItemForm/ItemFormGroup.tsx @@ -12,6 +12,7 @@ import CurrencyInput from "react-currency-input-field"; import { BsXLg } from "react-icons/bs"; import { useBudget } from "../../context/BudgetContext"; import { useConfig } from "../../context/ConfigContext"; +import { useDB } from "../../hooks/useDB"; import { calc, calcAvailable, @@ -21,7 +22,10 @@ import { parseLocaleNumber, roundBig, } from "../../utils"; -import { CalculateButton } from "../CalculateButton/CalculateButton"; +import { + CalculateButton, + ItemOperation, +} from "../CalculateButton/CalculateButton"; import { Expense } from "../TableCard/Expense"; import { Income } from "../TableCard/Income"; import { ItemForm } from "./ItemForm"; @@ -44,12 +48,33 @@ export function ItemFormGroup({ const deleteButtonRef = useRef(null); const valueRef = useRef(null); const { budget, setBudget } = useBudget(); + const { deleteCalcHist, saveCalcHist } = useDB(); const { intlConfig } = useConfig(); const isExpense = label === "Expenses"; const table = isExpense ? budget?.expenses : budget?.incomes; + function handleCalcHist( + operation: ItemOperation, + changeValue: number | undefined, + ) { + if (!budget) return; + const newItemForm = isExpense + ? budget.expenses.items.find((item) => item.id === itemForm.id) + : budget.incomes.items.find((item) => item.id === itemForm.id); + if (!newItemForm) return; + const calcHistID = `${budget.id}-${label}-${newItemForm.id}`; + saveCalcHist(calcHistID, { + id: calcHistID, + itemForm: newItemForm, + changeValue, + operation, + }).catch((e: unknown) => { + throw e; + }); + } + function handleChange( - operation: string, + operation: ItemOperation, value?: string, event?: React.ChangeEvent, changeValue?: number, @@ -79,6 +104,7 @@ export function ItemFormGroup({ saveInHistory = true; } setNeedsRerender(!needsRerender); + handleCalcHist(operation, changeValue); break; } @@ -89,6 +115,7 @@ export function ItemFormGroup({ draft.stats.withGoal = calcWithGoal(draft); draft.stats.saved = calcSaved(draft); }, budget); + setBudget(newState(), saveInHistory); } @@ -106,6 +133,11 @@ export function ItemFormGroup({ draft.stats.saved = calcSaved(draft); }, budget); setBudget(newState(), true); + + const calcHistID = `${budget.id}-${label}-${toBeDeleted.id}`; + deleteCalcHist(calcHistID).catch((e: unknown) => { + throw e; + }); } return ( diff --git a/src/db.ts b/src/db.ts index 5f5b6de..394a4f5 100644 --- a/src/db.ts +++ b/src/db.ts @@ -9,3 +9,8 @@ export const optionsDB = localforage.createInstance({ name: "guitos", storeName: "options", }); + +export const calcHistDB = localforage.createInstance({ + name: "guitos", + storeName: "calcHistDB", +}); diff --git a/src/hooks/useDB.ts b/src/hooks/useDB.ts index e0cfc63..31643d0 100644 --- a/src/hooks/useDB.ts +++ b/src/hooks/useDB.ts @@ -4,11 +4,12 @@ import { useEffect, useState } from "react"; import { Option } from "react-bootstrap-typeahead/types/types"; import { useParams } from "react-router-dom"; import { Budget } from "../components/Budget/Budget"; +import { CalculationHistoryItem } from "../components/CalculateButton/CalculateButton"; import { SearchOption } from "../components/NavBar/NavBar"; import { useBudget } from "../context/BudgetContext"; import { useConfig } from "../context/ConfigContext"; import { useGeneralContext } from "../context/GeneralContext"; -import { budgetsDB, optionsDB } from "../db"; +import { budgetsDB, calcHistDB, optionsDB } from "../db"; import { convertCsvToBudget, createBudgetNameList, @@ -304,6 +305,33 @@ export function useDB() { } } + async function getCalcHist(id: string): Promise { + let item; + await calcHistDB + .getItem(id) + .then((i) => { + item = i; + }) + .catch((e: unknown) => { + throw e; + }); + return item ?? []; + } + + async function saveCalcHist(id: string, item: CalculationHistoryItem) { + const calcHist = await getCalcHist(id); + const newCalcHist = [...calcHist, item]; + calcHistDB.setItem(id, newCalcHist).catch((e: unknown) => { + throw e; + }); + } + + async function deleteCalcHist(id: string) { + await calcHistDB.removeItem(id).catch((e: unknown) => { + throw e; + }); + } + function saveBudget(budget: Budget | undefined) { if (!budget) return; let list: Budget[] = []; @@ -345,5 +373,8 @@ export function useDB() { loadBudget, loadFromDb, options, + getCalcHist, + saveCalcHist, + deleteCalcHist, }; } diff --git a/src/utils.test.ts b/src/utils.test.ts index 3f090b0..3ea1f5f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -2,6 +2,7 @@ import Big from "big.js"; import Papa from "papaparse"; import { expect, test } from "vitest"; import { Budget } from "./components/Budget/Budget"; +import { ItemOperation } from "./components/CalculateButton/CalculateButton"; import { chromeLocalesList } from "./lists/chromeLocalesList"; import { currenciesMap } from "./lists/currenciesMap"; import { firefoxLocalesList } from "./lists/firefoxLocalesList"; @@ -193,13 +194,13 @@ reserves,reserves,200`); test("calc", () => { expect(calc(123.45, 100, "add")).eq(223.45); - expect(calc(123.45, 100, "sub")).eq(23.45); - expect(calc(123.45, 100, "mul")).eq(12345); - expect(calc(123.45, 100, "div")).eq(1.23); - expect(calc(0, 100, "sub")).eq(0); - expect(calc(0, 100, "mul")).eq(0); - expect(calc(0, 100, "div")).eq(0); - expect(() => calc(0, 100, "sqrt")).toThrow(); + expect(calc(123.45, 100, "subtract")).eq(23.45); + expect(calc(123.45, 100, "multiply")).eq(12345); + expect(calc(123.45, 100, "divide")).eq(1.23); + expect(calc(0, 100, "subtract")).eq(0); + expect(calc(0, 100, "multiply")).eq(0); + expect(calc(0, 100, "divide")).eq(0); + expect(() => calc(0, 100, "sqrt" as ItemOperation)).toThrow(); }); test("median", () => { diff --git a/src/utils.ts b/src/utils.ts index 1182833..176d59b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import Big from "big.js"; import { MutableRefObject } from "react"; import { Budget } from "./components/Budget/Budget"; import { CsvItem } from "./components/Budget/CsvItem"; +import { ItemOperation } from "./components/CalculateButton/CalculateButton"; import { ItemForm } from "./components/ItemForm/ItemForm"; import { SearchOption } from "./components/NavBar/NavBar"; import { currenciesMap } from "./lists/currenciesMap"; @@ -59,7 +60,7 @@ export function calcPercentage( export function calc( itemValue: number, change: number, - operation: string, + operation: ItemOperation, ): number { let total = 0; const isActionableChange = !isNaN(itemValue) && change > 0; @@ -71,13 +72,13 @@ export function calc( case "add": newValue = newValue.add(changeValue); break; - case "sub": + case "subtract": newValue = newValue.sub(changeValue); break; - case "mul": + case "multiply": newValue = newValue.mul(changeValue); break; - case "div": + case "divide": newValue = newValue.div(changeValue); break; default: