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) ); } -