diff --git a/api/app/account/routes.py b/api/app/account/account_routes.py
similarity index 97%
rename from api/app/account/routes.py
rename to api/app/account/account_routes.py
index 371e0a4..c2b86f9 100644
--- a/api/app/account/routes.py
+++ b/api/app/account/account_routes.py
@@ -17,7 +17,7 @@ def create_account(account: AccountCreate, session: SessionDep, current_user=Dep
@router.get("")
def read_accounts(session: SessionDep, current_user=Depends(get_current_user)) -> Page[AccountRead]:
- return paginate(session, Account.list())
+ return paginate(session, Account.list_accounts())
@router.get("/{account_id}")
def read_account(account_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead:
diff --git a/api/app/account/category_routes.py b/api/app/account/category_routes.py
new file mode 100644
index 0000000..8913fe8
--- /dev/null
+++ b/api/app/account/category_routes.py
@@ -0,0 +1,46 @@
+from uuid import UUID
+
+from fastapi import APIRouter, HTTPException, Depends
+from fastapi_pagination import Page
+from fastapi_pagination.ext.sqlmodel import paginate
+
+from account.models import Account, AccountRead, CategoryCreate, CategoryUpdate
+from db import SessionDep
+from user.manager import get_current_user
+
+router = APIRouter()
+
+@router.post("")
+def create_category(category: CategoryCreate, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead:
+ result = Account.create(category, session)
+ return result
+
+@router.get("")
+def read_categorys(session: SessionDep, current_user=Depends(get_current_user)) -> Page[AccountRead]:
+ return paginate(session, Account.list_categories())
+
+@router.get("/{category_id}")
+def read_category(category_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead:
+ category = Account.get(session, category_id)
+ if not category:
+ raise HTTPException(status_code=404, detail="Category not found")
+ return category
+
+@router.put("/{category_id}")
+def update_category(category_id: UUID, category: CategoryUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> AccountRead:
+ db_category = Account.get(session, category_id)
+ if not db_category:
+ raise HTTPException(status_code=404, detail="Category not found")
+
+ category_data = category.model_dump(exclude_unset=True)
+ category = Account.update(session, db_category, category_data)
+ return category
+
+@router.delete("/{category_id}")
+def delete_category(category_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
+ category = Account.get(session, category_id)
+ if not category:
+ raise HTTPException(status_code=404, detail="Category not found")
+
+ Account.delete(session, category)
+ return {"ok": True}
diff --git a/api/app/account/models.py b/api/app/account/models.py
index d1cdbc0..46de73f 100644
--- a/api/app/account/models.py
+++ b/api/app/account/models.py
@@ -3,6 +3,61 @@ from enum import Enum
from sqlmodel import Field, SQLModel, select
+
+class AccountBase(SQLModel):
+ name: str = Field(index=True)
+ type: str = Field(index=True)
+
+class AccountBaseId(AccountBase):
+ id: UUID | None = Field(default_factory=uuid4, primary_key=True)
+
+class Account(AccountBaseId, table=True):
+
+ @classmethod
+ def create(cls, account, session):
+ account_db = cls.model_validate(account)
+ session.add(account_db)
+ session.commit()
+ session.refresh(account_db)
+
+ return account_db
+
+ @classmethod
+ def list(cls):
+ return select(Account)
+
+ @classmethod
+ def list_accounts(cls):
+ return cls.list().where(
+ Account.type.not_in_([v.value for v in CategoryType])
+ )
+
+ @classmethod
+ def list_categories(cls):
+ return cls.list().where(
+ Account.type.in_([v.value for v in CategoryType])
+ )
+
+ @classmethod
+ def get(cls, session, account_id):
+ return session.get(Account, account_id)
+
+ @classmethod
+ def update(cls, session, account_db, account_data):
+ account_db.sqlmodel_update(account_data)
+ session.add(account_db)
+ session.commit()
+ session.refresh(account_db)
+ return account_db
+
+ @classmethod
+ def delete(cls, session, account):
+ session.delete(account)
+ session.commit()
+
+class AccountRead(AccountBaseId):
+ pass
+
class AccountType(Enum):
Asset = "Asset" # < Denotes a generic asset account.
Checkings = "Checkings" # < Standard checking account
@@ -24,53 +79,36 @@ class AccountType(Enum):
Income = "Income" # < Denotes an income account
Expense = "Expense" # < Denotes an expense account
-class AccountBase(SQLModel):
- name: str = Field(index=True)
- type: AccountType = Field(index=True)
+class Asset(Enum):
+ Asset = "Asset"
+ Checkings = "Checkings"
+ Savings = "Savings"
+ Cash = "Cash"
-class AccountBaseId(AccountBase):
- id: UUID | None = Field(default_factory=uuid4, primary_key=True)
-
-class Account(AccountBaseId, table=True):
-
- @classmethod
- def create(cls, account, session):
- account_db = cls.model_validate(account)
- session.add(account_db)
- session.commit()
- session.refresh(account_db)
-
- return account_db
-
- @classmethod
- def list(cls):
- return select(Account)
-
- @classmethod
- def get(cls, session, account_id):
- return session.get(Account, account_id)
-
- @classmethod
- def update(cls, session, account_db, account_data):
- account_db.sqlmodel_update(account_data)
- session.add(account_db)
- session.commit()
- session.refresh(account_db)
- return account_db
-
- @classmethod
- def delete(cls, session, account):
- session.delete(account)
- session.commit()
-
-class AccountRead(AccountBaseId):
- pass
+class Liability(Enum):
+ Liability = "Liability"
+ CreditCard = "CreditCard"
+ Loan = "Loan"
+ Investment = "Investment"
class AccountWrite(AccountBase):
- pass
+ type: Asset | Liability = Field()
class AccountCreate(AccountWrite):
pass
class AccountUpdate(AccountWrite):
pass
+
+class CategoryType(Enum):
+ Income = "Income"
+ Expense = "Expense"
+
+class CategoryWrite(AccountBase):
+ type: CategoryType = Field()
+
+class CategoryCreate(CategoryWrite):
+ pass
+
+class CategoryUpdate(CategoryWrite):
+ pass
\ No newline at end of file
diff --git a/api/app/main.py b/api/app/main.py
index ceb480c..12552be 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -8,7 +8,8 @@ from fastapi_pagination import add_pagination
from db import create_db_and_tables
from user import user_router, auth_router, create_admin_account
-from account.routes import router as account_router
+from account.account_routes import router as account_router
+from account.category_routes import router as category_router
from payee.routes import router as payee_router
from transaction.routes import router as transaction_router
@@ -39,6 +40,7 @@ app.add_middleware(
app.include_router(auth_router, prefix="/auth", tags=["auth"], )
app.include_router(user_router, prefix="/users", tags=["users"])
app.include_router(account_router, prefix="/accounts", tags=["accounts"])
+app.include_router(category_router, prefix="/categories", tags=["categories"])
app.include_router(payee_router, prefix="/payees", tags=["payees"])
app.include_router(transaction_router, prefix="/transactions", tags=["transactions"])
diff --git a/gui/app/src/App.tsx b/gui/app/src/App.tsx
index a378d41..f9ddabf 100644
--- a/gui/app/src/App.tsx
+++ b/gui/app/src/App.tsx
@@ -25,6 +25,7 @@ import { useFormContext } from "react-hook-form";
import { PostList, PostCreate, PostEdit } from "./pages/posts";
import { AccountList, AccountCreate, AccountEdit } from "./pages/accounts";
+import { CategoryList, CategoryCreate, CategoryEdit } from "./pages/categories";
import { PayeeCreate, PayeeEdit, PayeeList } from "./pages/payees";
import { TransactionCreate, TransactionEdit, TransactionList } from "./pages/transactions";
@@ -81,6 +82,12 @@ const App: React.FC = () => {
edit: "/accounts/edit/:id",
create: "/accounts/create",
},
+ {
+ name: "categories",
+ list: "/categories",
+ edit: "/categories/edit/:id",
+ create: "/categories/create",
+ },
{
name: "payees",
list: "/payees",
@@ -129,6 +136,11 @@ const App: React.FC = () => {
} />
} />
+
+ } />
+ } />
+ } />
+
} />
} />
diff --git a/gui/app/src/pages/categories/create.tsx b/gui/app/src/pages/categories/create.tsx
new file mode 100644
index 0000000..3fa8346
--- /dev/null
+++ b/gui/app/src/pages/categories/create.tsx
@@ -0,0 +1,12 @@
+import {CrudForm} from "../../common/crud/crud-form";
+import {CategoryEdit} from "./edit";
+
+
+export const CategoryCreate: React.FC = () => {
+ return (
+
+ );
+};
diff --git a/gui/app/src/pages/categories/edit.tsx b/gui/app/src/pages/categories/edit.tsx
new file mode 100644
index 0000000..831604b
--- /dev/null
+++ b/gui/app/src/pages/categories/edit.tsx
@@ -0,0 +1,15 @@
+import { CrudForm } from "../../common/crud/crud-form";
+import { useParams } from "react-router"
+
+
+export const CategoryEdit: React.FC = () => {
+ const { id } = useParams()
+
+ return (
+
+ );
+};
diff --git a/gui/app/src/pages/categories/index.tsx b/gui/app/src/pages/categories/index.tsx
new file mode 100644
index 0000000..b9af745
--- /dev/null
+++ b/gui/app/src/pages/categories/index.tsx
@@ -0,0 +1,3 @@
+export * from "./list";
+export * from "./create";
+export * from "./edit";
diff --git a/gui/app/src/pages/categories/list.tsx b/gui/app/src/pages/categories/list.tsx
new file mode 100644
index 0000000..f9d4b75
--- /dev/null
+++ b/gui/app/src/pages/categories/list.tsx
@@ -0,0 +1,49 @@
+import {DeleteButton, EditButton, List, useDataGrid} from "@refinedev/mui";
+import React from "react";
+
+import { DataGrid, type GridColDef } from "@mui/x-data-grid";
+
+import type { IAccount } from "../../interfaces";
+import { ButtonGroup } from "@mui/material";
+
+export const CategoryList: React.FC = () => {
+ const { dataGridProps } = useDataGrid();
+
+ const columns = React.useMemo[]>(
+ () => [
+ { field: "id", headerName: "ID" },
+ { field: "name", headerName: "Name", flex: 1 },
+ { field: "type", headerName: "Type", flex: 0.3 },
+ {
+ field: "actions",
+ headerName: "Actions",
+ display: "flex",
+ renderCell: function render({ row }) {
+ return (
+
+
+
+
+ );
+ },
+ align: "center",
+ headerAlign: "center",
+ },
+ ],
+ [],
+ );
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/gui/app/src/providers/data-provider.tsx b/gui/app/src/providers/data-provider.tsx
index ea5d133..c210512 100644
--- a/gui/app/src/providers/data-provider.tsx
+++ b/gui/app/src/providers/data-provider.tsx
@@ -1,6 +1,5 @@
import type { DataProvider } from "@refinedev/core";
-//const API_URL = "https://api.fake-rest.refine.dev";
const API_URL = "http://localhost:8000";
const fetcher = async (url: string, options?: RequestInit) => {
diff --git a/gui/app/src/providers/jsonschema-provider.tsx b/gui/app/src/providers/jsonschema-provider.tsx
index bccecd2..24ff396 100644
--- a/gui/app/src/providers/jsonschema-provider.tsx
+++ b/gui/app/src/providers/jsonschema-provider.tsx
@@ -31,8 +31,9 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
if (is_reference(prop)) {
resolveReference(rawSchemas, resource, prop);
} else if (is_union(prop)) {
- for (let i in prop.oneOf) {
- resolveReference(rawSchemas, resource, prop.oneOf[i]);
+ const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
+ for (let i in union) {
+ resolveReference(rawSchemas, resource, union[i]);
}
} else if (is_enum(prop)) {
for (let i in prop.allOf) {
@@ -92,11 +93,11 @@ function is_array(prop: any) {
}
function is_union(prop: any) {
- return prop.hasOwnProperty('oneOf');
+ return prop.hasOwnProperty('oneOf') || prop.hasOwnProperty('anyOf');
}
function is_enum(prop: any) {
- return prop.hasOwnProperty('allOf');
+ return prop.hasOwnProperty('enum');
}
function get_reference_name(prop: any) {
@@ -112,7 +113,8 @@ function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_na
let subresourceName = get_reference_name(resource);
return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
} else if (is_union(resource)) {
- for (const ref of resource.oneOf!) {
+ const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
+ for (const ref of union) {
return has_descendant(rawSchemas, ref, property_name)
}
} else if (is_enum(resource)) {
@@ -183,4 +185,3 @@ function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path
path.substring(pointFirstPosition + 1)
);
}
-