feat: budget crud actions

This commit is contained in:
Max Koon
2025-12-06 23:02:28 -05:00
parent 76f2a43bd0
commit 27f6e627d4
20 changed files with 445 additions and 113 deletions

View File

@@ -1,26 +1,39 @@
import { use } from "react";
import { View, Text } from "react-native";
import { use, useRef, useState } from "react";
import { View, Text, TextInput } from "react-native";
import { RouterContext } from ".";
import { queries, type Mutators, type Schema } from "@money/shared";
import {
queries,
type Category,
type Mutators,
type Schema,
} from "@money/shared";
import { useQuery, useZero } from "@rocicorp/zero/react";
import * as Table from "../components/Table";
import { Button } from "../components/Button";
import * as Dialog from "../components/Dialog";
const COLUMNS: Table.Column[] = [{ name: "label", label: "Name" }];
const COLUMNS: Table.Column[] = [
{ name: "label", label: "Name" },
{ name: "week", label: "Week" },
{ name: "month", label: "Month" },
{ name: "year", label: "Year" },
{ name: "order", label: "Order" },
];
export function Budget() {
const { auth } = use(RouterContext);
const [budgets] = useQuery(queries.getBudgets(auth));
// const [items] = useQuery(queries.getBudgetCategories(auth));
const items: any[] = [];
const [renaming, setRenaming] = useState<Category>();
const z = useZero<Schema, Mutators>();
const refText = useRef("");
const newBudget = () => {
const id = new Date().getTime().toString();
const categoryId = new Date().getTime().toString();
z.mutate.budget.create({
id,
categoryId,
});
};
@@ -45,21 +58,89 @@ export function Budget() {
const budget = budgets[0]!;
const data = budget.categories.slice().map((category) => {
const { amount } = category;
const week = amount;
const month = amount * 4;
const year = amount * 12;
return {
...category,
...{
week,
month,
year,
},
};
});
const newCategory = ({ index }: { index: number }) => {
const id = new Date().getTime().toString();
z.mutate.budget.createCategory({
id,
budgetId: budget.id,
order: index - 1,
});
};
const deleteCategory = ({ selected }: { selected: { id: string }[] }) => {
for (const { id } of selected) {
z.mutate.budget.deleteCategory({ id });
}
};
const renameCategory = ({ selected }: { selected: Category[] }) => {
for (const category of selected) {
setRenaming(category);
}
};
return (
<>
<View>
<Text style={{ fontFamily: "mono" }}>
<Dialog.Provider
visible={renaming != undefined}
close={() => setRenaming(undefined)}
>
<Dialog.Content>
<Text style={{ fontFamily: "mono" }}>Edit Category</Text>
<TextInput
style={{ fontFamily: "mono" }}
autoFocus
selectTextOnFocus
defaultValue={renaming?.label}
onChangeText={(t) => {
refText.current = t;
}}
onKeyPress={(e) => {
if (!renaming) return;
if (e.nativeEvent.key == "Enter") {
z.mutate.budget.updateCategory({
id: renaming.id,
label: refText.current,
});
setRenaming(undefined);
} else if (e.nativeEvent.key == "Escape") {
setRenaming(undefined);
}
}}
/>
</Dialog.Content>
</Dialog.Provider>
<View style={{ alignItems: "flex-start" }}>
<Text style={{ fontFamily: "mono", textAlign: "left" }}>
Selected Budget: {budget.label}
</Text>
</View>
<Table.Provider
data={items}
data={data}
columns={COLUMNS}
onKey={(event) => {
if (event.name == "n" && event.shift) {
newBudget();
}
}}
shortcuts={[
{ key: "i", handler: newCategory },
{ key: "d", handler: deleteCategory },
{ key: "r", handler: renameCategory },
]}
>
<View style={{ flex: 1 }}>
<View style={{ flexShrink: 0 }}>

View File

@@ -1,11 +1,15 @@
import { createContext, use, useEffect, useRef } from "react";
import { createContext, use, type ReactNode } from "react";
import { Transactions } from "./transactions";
import { View } from "react-native";
import { Settings } from "./settings";
import type { AuthData } from "@money/shared/auth";
import { Budget } from "./budget";
import { ShortcutProvider, ShortcutDebug, keysStore } from "../lib/shortcuts";
import { useShortcut } from "../lib/shortcuts/hooks";
import {
ShortcutProvider,
ShortcutDebug,
useShortcut,
type KeyName,
} from "../lib/shortcuts";
const PAGES = {
"/": {
@@ -24,7 +28,10 @@ const PAGES = {
"/family": {},
},
},
};
} satisfies Record<
string,
{ screen: ReactNode; key: KeyName; children?: Record<string, unknown> }
>;
type Join<A extends string, B extends string> = `${A}${B}` extends `${infer X}`
? X
@@ -32,14 +39,14 @@ type Join<A extends string, B extends string> = `${A}${B}` extends `${infer X}`
type ChildRoutes<Parent extends string, Children> = {
[K in keyof Children & string]: K extends `/${string}`
? Join<Parent, K>
: never;
? Join<Parent, K>
: never;
}[keyof Children & string];
type Routes<T> = {
[K in keyof T & string]:
| K
| (T[K] extends { children: infer C } ? ChildRoutes<K, C> : never);
| K
| (T[K] extends { children: infer C } ? ChildRoutes<K, C> : never);
}[keyof T & string];
export type Route = Routes<typeof PAGES>;
@@ -53,7 +60,7 @@ interface RouterContextType {
export const RouterContext = createContext<RouterContextType>({
auth: null,
route: "/",
setRoute: () => {},
setRoute: () => { },
});
type AppProps = {
@@ -84,8 +91,8 @@ function Main() {
route in PAGES
? (route as keyof typeof PAGES)
: (Object.keys(PAGES)
.sort((a, b) => b.length - a.length)
.find((p) => route.startsWith(p)) as keyof typeof PAGES);
.sort((a, b) => b.length - a.length)
.find((p) => route.startsWith(p)) as keyof typeof PAGES);
return (
<View style={{ backgroundColor: "white", flex: 1 }}>

View File

@@ -4,6 +4,7 @@ import { RouterContext, type Route } from ".";
import { General } from "./settings/general";
import { Accounts } from "./settings/accounts";
import { Family } from "./settings/family";
import { useShortcut } from "../lib/shortcuts";
type SettingsRoute = Extract<Route, `/settings${string}`>;
@@ -30,27 +31,24 @@ type Tab = keyof typeof TABS;
export function Settings() {
const { route, setRoute } = use(RouterContext);
// useKeyboard(
// (key) => {
// if (key.name == "h") {
// const currentIdx = Object.entries(TABS).findIndex(
// ([tabRoute, _]) => tabRoute == route,
// );
// const routes = Object.keys(TABS) as SettingsRoute[];
// const last = routes[currentIdx - 1];
// if (!last) return;
// setRoute(last);
// } else if (key.name == "l") {
// const currentIdx = Object.entries(TABS).findIndex(
// ([tabRoute, _]) => tabRoute == route,
// );
// const routes = Object.keys(TABS) as SettingsRoute[];
// const next = routes[currentIdx + 1];
// if (!next) return;
// setRoute(next);
// }
// },
// );
useShortcut("h", () => {
const currentIdx = Object.entries(TABS).findIndex(
([tabRoute, _]) => tabRoute == route,
);
const routes = Object.keys(TABS) as SettingsRoute[];
const last = routes[currentIdx - 1];
if (!last) return;
setRoute(last);
});
useShortcut("l", () => {
const currentIdx = Object.entries(TABS).findIndex(
([tabRoute, _]) => tabRoute == route,
);
const routes = Object.keys(TABS) as SettingsRoute[];
const next = routes[currentIdx + 1];
if (!next) return;
setRoute(next);
});
return (
<View style={{ flexDirection: "row" }}>