Compare commits

..

12 Commits

20 changed files with 273 additions and 85 deletions

View File

@@ -31,8 +31,7 @@ async def create(schema: ContractCreate, reg=Depends(get_authed_tenant_registry)
contract_dict = schema.model_dump()
del(contract_dict['draft_id'])
lawyer = await Entity.get(reg.db, reg.user.entity_id)
contract_dict['lawyer'] = lawyer.model_dump()
contract_dict['lawyer'] = reg.partner.model_dump()
contract_dict['name'] = draft.name
contract_dict['title'] = draft.title

View File

@@ -1,13 +1,14 @@
import datetime
from typing import List
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
from firm.contract.models import ContractDraft, DraftProvision, DraftParty, Contract
from firm.entity.models import Entity
from firm.core.schemas import Writer, Reader
from firm.core.models import DictionaryEntry
from firm.core.models import DictionaryEntry, ForeignKey
class ContractDraftRead(Reader, ContractDraft):
@@ -68,7 +69,7 @@ class ContractRead(Reader, Contract):
class ContractCreate(Writer):
date: datetime.date
location: str
draft_id: str
draft_id: PydanticObjectId = ForeignKey(resource="contracts/drafts", schema="ContractDraft")
class ContractInit(BaseModel):
date: datetime.date

View File

@@ -1,11 +1,15 @@
from fastapi import HTTPException, Depends
from hub.auth import get_current_user
from firm.current_firm import CurrentFirmModel, Partner
from firm.db import get_db_client
from firm.current_firm import CurrentFirmModel
from firm.entity.models import Entity
class Registry:
user = None
partner = None
def __init__(self, db_client, instance, firm):
self.db = db_client[f"tenant_{instance}_{firm}"]
@@ -14,11 +18,14 @@ class Registry:
self.current_firm = CurrentFirmModel.get_current(self.db)
def set_user(self, user):
async def set_user(self, user):
for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm:
partner = await Partner.get_by_user_id(self.db, user.id)
partner_entity = await Entity.get(self.db, partner.entity_id)
self.user = user
self.db.user = user
self.partner = partner_entity
self.db.partner = partner_entity
return
raise PermissionError
@@ -30,9 +37,9 @@ async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db
return registry
def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry:
async def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry:
try:
registry.set_user(user)
await registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
@@ -44,7 +51,7 @@ async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends
raise HTTPException(status_code=409, detail="Firm configuration already exists")
try:
registry.set_user(user)
await registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")

View File

@@ -37,7 +37,7 @@ class CrudDocument(BaseModel):
@classmethod
async def create(cls, db, create_schema):
model_dict = create_schema.model_dump() | {"created_by": db.user.id, "updated_by": db.user.id}
model_dict = create_schema.model_dump() | {"created_by": db.partner.id, "updated_by": db.partner.id}
document = cls.model_validate(model_dict).model_dump(mode="json")
result = await cls._get_collection(db).insert_one(document)
@@ -52,8 +52,13 @@ class CrudDocument(BaseModel):
}
@classmethod
def list(cls, db):
return cls._get_collection(db).find({})
async def list(cls, db, criteria={}):
result = []
for document in await cls._get_collection(db).find(criteria).to_list():
document["id"] = document.pop("_id")
result.append(cls.model_validate(document))
return result
@classmethod
async def get(cls, db, model_id):
@@ -66,7 +71,7 @@ class CrudDocument(BaseModel):
@classmethod
async def update(cls, db, model, update_schema):
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.user.id}
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.partner.id}
update_query = {
"$set": {field: value for field, value in model_dict.items() if field!= "id" }
}

View File

@@ -36,14 +36,15 @@ class CurrentFirmModel(CrudDocument):
class CurrentFirmSchemaRead(Reader):
entity: EntityRead
partner: EntityRead
partner_list: list[EntityRead]
instance: str
firm: str
primary_color: str
secondary_color: str
@classmethod
def from_model_and_entities(cls, model, entity, partner):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner)
def from_model_and_entities(cls, model, entity, partner, partner_list):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner, partner_list=partner_list)
return schema
class CurrentFirmSchemaCreate(Writer):

View File

@@ -10,10 +10,17 @@ current_firm_router = APIRouter()
@current_firm_router.get("/", response_model=CurrentFirmSchemaRead, response_description=f"Current Firm records retrieved")
async def read(reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
document = await CurrentFirmModel.get_current(reg.db)
entity = await Entity.get(reg.db, document.entity_id)
firm_entity = await Entity.get(reg.db, document.entity_id)
partner = await Partner.get_by_user_id(reg.db, reg.user.id)
partner = await Entity.get(reg.db, partner.entity_id)
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(entity), EntityRead.from_model(partner))
partner_list = await Partner.list(reg.db)
partner_list = await Entity.list(reg.db, {"_id": {"$in": [p.entity_id for p in partner_list]}})
return CurrentFirmSchemaRead.from_model_and_entities(
document,
EntityRead.from_model(firm_entity),
EntityRead.from_model(partner),
[EntityRead.from_model(p) for p in partner_list]
)
@current_firm_router.post("/", response_description=f"Current Firm added to the database")
async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_registry)) -> CurrentFirmSchemaRead:

View File

@@ -35,6 +35,7 @@
"@tiptap/extension-underline": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"dayjs": "^1.11.13",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",

View File

@@ -31,6 +31,7 @@
"@tiptap/extension-underline": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"dayjs": "^1.11.13",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",

View File

@@ -0,0 +1,33 @@
import { useTranslation } from "@refinedev/core";
import { useContext } from "react";
import { FirmContext } from "../contexts/FirmContext";
type CartoucheProps = {
record: any
}
const Cartouche = (props: CartoucheProps) => {
const { record } = props;
const { translate: t } = useTranslation();
return (
<>
<ul>
{ record.created_at && <li>{t("resource.created_at")}: {record.created_at} {t("resource.created_at")}: <AuthorField partnerId={record.created_by} /></li> }
{ record.updated_at && <li>{t("resource.updated_at")}: {record.updated_at} {t("resource.updated_by")}: <AuthorField partnerId={record.updated_by} /></li> }
</ul>
</>
)
}
export default Cartouche;
const AuthorField = (props: {partnerId: string})=> {
const { partnerId } = props;
const { partnerMap } = useContext(FirmContext);
const { translate: t } = useTranslation();
if (partnerMap && partnerMap.has(partnerId)) {
return <>{ partnerMap.get(partnerId) }</>
}
return <>{t("REDACTED")}</>
}

View File

@@ -1,9 +1,12 @@
import React, { createContext, PropsWithChildren } from 'react';
import { IFirm } from "../interfaces";
import { useParams } from "react-router";
import { useOne } from "@refinedev/core";
import { CircularProgress } from "@mui/material";
type FirmContextType = {
currentFirm: IFirm,
partnerMap?: Map<string, string>
}
export const FirmContext = createContext<FirmContextType>(
@@ -12,13 +15,27 @@ export const FirmContext = createContext<FirmContextType>(
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { instance, firm } = useParams<IFirm>()
const { instance, firm } = useParams<IFirm>();
const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false});
if (instance === undefined || firm === undefined) {
return "Error"
}
if (isLoading) {
return <CircularProgress />
}
let value: FirmContextType = {
currentFirm: {instance, firm}
}
if (!isError || error?.statusCode != 405) {
value.currentFirm.entity = data?.data.entity;
value.partnerMap = new Map(data?.data.partner_list.map((item: any) => [item.id, item.label]));
}
return (
<FirmContext.Provider value={{currentFirm: {instance, firm}}} >
<FirmContext.Provider value={value} >
{ children }
</FirmContext.Provider>
);

View File

@@ -2,6 +2,7 @@
export type IFirm = {
instance: string,
firm: string
entity?: any
}
type User = {

View File

@@ -8,32 +8,23 @@ import { BaseForm } from "./base-form";
type CrudFormProps = {
schemaName: string,
uiSchema?: UiSchema,
resourceBasePath?: string,
resource: string,
id?: string,
onSuccess?: (data: any) => void,
record?: any,
resourceBasePath: string,
onSubmit: (data: any) => void,
defaultValue?: any,
children?: ReactNode
}
export const CrudForm: React.FC<CrudFormProps> = (props) => {
const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess, defaultValue, children } = props;
const { onFinish, query, formLoading } = useForm({
resource: resourceBasePath == "" ? resource : `${resourceBasePath}/${resource}`,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } },
});
const { schemaName, uiSchema, record, resourceBasePath, defaultValue, children, onSubmit } = props;
const [schema, setSchema] = useState({});
const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceSchema = id === undefined ? await jsonschemaProvider.getCreateResourceSchema(schemaName)
: await jsonschemaProvider.getCardResourceSchema(schemaName);
const resourceSchema = record === undefined ? await jsonschemaProvider.getCreateResourceSchema(schemaName)
: await jsonschemaProvider.getUpdateResourceSchema(schemaName);
setSchema(resourceSchema);
setSchemaLoading(false);
} catch (error) {
@@ -44,19 +35,18 @@ export const CrudForm: React.FC<CrudFormProps> = (props) => {
fetchSchema();
}, []);
if(formLoading || schemaLoading) {
if(schemaLoading) {
return <CircularProgress />
}
const record = query?.data?.data || defaultValue;
return (
<BaseForm
schema={schema}
uiSchema={uiSchema}
formData={record}
formData={record || defaultValue}
resourceBasePath={resourceBasePath}
onSubmit={
(data: any) => onFinish(data)
(data: any) => onSubmit(data)
}
children={children}
/>

View File

@@ -25,9 +25,11 @@ declare module "@tiptap/core" {
export default Extension.create<IndentOptions>({
name: "indent",
defaultOptions: {
addOptions() {
return {
types: ["paragraph", "heading"],
margin: 40
}
},
addGlobalAttributes() {

View File

@@ -32,6 +32,21 @@ export const jsonschemaProvider = {
return readSchema
},
getReadOnlyResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
const updateSchema = await getResourceSchema(`${resourceName}Update`);
const readSchema = await getResourceSchema(`${resourceName}Read`);
for (let prop_name in readSchema.properties) {
if (updateSchema.hasOwnProperty(prop_name)) {
delete readSchema.properties[prop_name];
} else {
readSchema.properties[prop_name].readOnly = true;
}
}
return readSchema
},
getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Update`)
},
@@ -59,10 +74,12 @@ function convertCamelToSnake(str: string): string {
}
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
let resource;
if (rawSchemas.components.schemas[resourceName] === undefined) {
throw new Error(`Resource "${resourceName}" not found in schema.`);
}
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, ""));
resource = structuredClone(rawSchemas.components.schemas[resourceName]);
let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
@@ -110,23 +127,35 @@ function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference:
function changePropertiesOrder(resource: any) {
let created_at;
let created_by;
let updated_at;
let updated_by;
let new_properties: any = {};
for (let prop_name in resource.properties) {
if (prop_name == 'created_at') {
created_at = resource.properties[prop_name];
} else if (prop_name == 'created_by') {
created_by = resource.properties[prop_name];
} else if (prop_name == 'updated_at') {
updated_at = resource.properties[prop_name];
} else {
} else if (prop_name == 'updated_by') {
updated_by = resource.properties[prop_name];
}else {
new_properties[prop_name] = resource.properties[prop_name];
}
}
if (created_at) {
new_properties['created_at'] = created_at;
}
if (created_by) {
new_properties['created_by'] = created_by;
}
if (updated_at) {
new_properties['updated_at'] = updated_at;
}
if (updated_by) {
new_properties['updated_by'] = updated_by;
}
resource.properties = new_properties
}

View File

@@ -1,9 +1,8 @@
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Contract = {
export type Contract = {
id: string,
label: string
}
@@ -13,7 +12,6 @@ export const ContractRoutes = () => {
<Routes>
<Route index element={ <ListContract /> } />
<Route path="/edit/:record_id" element={ <EditContract /> } />
<Route path="/create" element={ <CreateContract /> } />
</Routes>
);
}
@@ -28,7 +26,3 @@ const ListContract = () => {
const EditContract = () => {
return <Edit<Contract> resource={`contracts`} schemaName={"Contract"} />
}
const CreateContract = () => {
return <New<Contract> resource={`contracts`} schemaName={"Contract"} />
}

View File

@@ -1,7 +1,7 @@
import { Route, Routes } from "react-router";
import { Navigate, Route, Routes, useParams } from "react-router";
import { CircularProgress } from "@mui/material";
import React, { useContext, useState } from "react";
import { useOne } from "@refinedev/core";
import { useOne, useTranslation } from "@refinedev/core";
import { BaseForm } from "../../lib/crud/components/base-form";
import { ForeignKeyReference, ForeignKeySchema } from "../../lib/crud/components/widgets/foreign-key";
@@ -9,6 +9,8 @@ import { FirmContext } from "../../contexts/FirmContext";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
import { Contract } from "./ContractRoutes";
import dayjs from "dayjs";
type Draft = {
id: string,
@@ -33,7 +35,63 @@ const ListDraft = () => {
}
const EditDraft = () => {
return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
const { currentFirm } = useContext(FirmContext);
const { record_id } = useParams();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { data, isLoading } = useOne({
resource: `${resourceBasePath}/contracts/drafts`,
id: record_id
})
if (isLoading) {
return <CircularProgress />
}
if (!data?.data) {
return <Navigate to="../" />
}
const draft = data?.data
const readOnly = draft.status === "published";
const uiSchema = {
"ui:readonly": readOnly
}
return (
<>
<Edit<Draft> resource={"contracts/drafts"} schemaName={"ContractDraft"} uiSchema={uiSchema} />
<ContractCreate draft={draft}></ContractCreate>
</>
)
}
const ContractCreate = (props: { draft: any}) => {
const { translate: t } = useTranslation();
const { draft } = props;
if (draft.status === "published") {
return <h4>{t("resource.draft.already_published") }</h4>
}
if (draft.status === "in_progress") {
return (
<>
<h4>{ t("resource.draft.todo") + ":" }</h4>
<ul>{ draft.todo.map((item: any) => <li>{ item }</li>) }</ul>
</>
)
}
return <New<Contract>
resource={"contracts"}
schemaName={"Contract"}
defaultValue={{
date: dayjs().format("YYYY-MM-DD"),
location: "Los Santos, SA",
draft_id: draft.id
}}
uiSchema={{ draft_id: { 'ui:widget': 'hidden' } }}
/>
}
type ForeignKeySubSchema = ForeignKeySchema & {

View File

@@ -1,13 +1,14 @@
import { UiSchema } from "@rjsf/utils";
import { useParams } from "react-router";
import { useContext } from "react";
import { Button } from "@mui/material";
import DeleteIcon from '@mui/icons-material/Delete';
import { useParams, Navigate } from "react-router";
import { Button, CircularProgress } from "@mui/material";
import Stack from "@mui/material/Stack";
import SaveIcon from '@mui/icons-material/Save';
import { useForm, useTranslation } from "@refinedev/core";
import { DeleteButton } from "@refinedev/mui";
import { FirmContext } from "../../../contexts/FirmContext";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import Stack from "@mui/material/Stack";
import { DeleteButton } from "@refinedev/mui";
import Cartouche from "../../../components/Cartouche";
type EditProps = {
resource: string,
@@ -18,17 +19,36 @@ type EditProps = {
const Edit = <T,>(props: EditProps) => {
const { schemaName, resource, uiSchema } = props;
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { record_id } = useParams();
const { onFinish, query, formLoading } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "edit",
redirect: "show",
id: record_id,
});
if (formLoading) {
return <CircularProgress />
}
if (!query?.data?.data) {
return <Navigate to="../" />
}
const record = query.data.data;
return (
<>
<h2>{record.label}</h2>
<Cartouche record={record}/>
<CrudForm
resourceBasePath={resourceBasePath}
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
id={record_id}
record={record}
onSubmit={(data: any) => onFinish(data)}
>
<Stack
direction="row"
@@ -37,7 +57,7 @@ const Edit = <T,>(props: EditProps) => {
justifyContent: "space-between",
alignItems: "center",
}}>
<Button type='submit' variant="contained" size="large"><SaveIcon />Save</Button>
<Button type='submit' variant="contained" size="large"><SaveIcon />{t("buttons.save")}</Button>
<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>
</Stack>
</CrudForm>

View File

@@ -1,7 +1,10 @@
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { UiSchema } from "@rjsf/utils";
import { useContext } from "react";
import { useForm, useTranslation } from "@refinedev/core";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { FirmContext } from "../../../contexts/FirmContext";
import SaveIcon from "@mui/icons-material/Save";
import { Button } from "@mui/material";
type NewProps = {
resource: string,
@@ -13,16 +16,25 @@ type NewProps = {
const New = <T,>(props: NewProps) => {
const { schemaName, resource, uiSchema, defaultValue } = props;
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { onFinish } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "create",
redirect: "show",
});
return (
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
defaultValue={defaultValue}
/>
onSubmit={(data: any) => onFinish(data)}
>
<Button type='submit' variant="contained" size="large"><SaveIcon />{t("buttons.create")}</Button>
</CrudForm>
)
}

View File

@@ -1,8 +1,8 @@
import { Route, Routes, Link } from "react-router";
import React, { useContext } from "react";
import { useForm, useOne, useTranslation } from "@refinedev/core";
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
import { Header } from "../../components";
import { useOne } from "@refinedev/core";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { IFirm } from "../../interfaces";
import { EntityRoutes } from "./EntityRoutes";
@@ -33,15 +33,7 @@ export const FirmRoutes = () => {
const FirmHome = () => {
const { currentFirm } = useContext(FirmContext);
const { data: firm, isError, error, isLoading } = useOne({resource: 'firm', id: `${currentFirm.instance}/${currentFirm.firm}/`, errorNotification: false})
if (isLoading) {
return <h1>Loading...</h1>
}
if (isError && error?.statusCode == 405) {
return <FirmInitForm currentFirm={currentFirm} />
}
const { translate: t } = useTranslation();
return (
<>
@@ -64,16 +56,26 @@ type FirmInitFormPros = {
const FirmInitForm = (props: FirmInitFormPros) => {
const { currentFirm } = props;
const { translate: t } = useTranslation();
const resourceBasePath = `firm`
const { onFinish } = useForm({
resource: `${resourceBasePath}/${currentFirm.instance}/${currentFirm.firm}`,
action: "create",
redirect: "show",
});
return (
<>
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
<CrudForm
schemaName={"CurrentFirmSchemaCreate"}
resource={`firm/${currentFirm.instance}/${currentFirm.firm}/`}
schemaName={"CurrentFirmSchema"}
resourceBasePath={resourceBasePath}
defaultValue={{corporation: {entity_data: {activity: t("firm.default_activity") }}}}
uiSchema={{
corporation: {entity_data: {employees: {"ui:style": {"display": "none"}}}},
}}
onSubmit={(data: any) => onFinish(data)}
/>
</>
)

View File

@@ -1,6 +1,6 @@
import { useInvalidateAuthStore } from "@refinedev/core";
import { useForm, useInvalidateAuthStore } from "@refinedev/core";
import { CrudForm } from "../../lib/crud/components/crud-form";
import {empty_user} from "../../providers/auth-provider";
import { empty_user } from "../../providers/auth-provider";
export const CreateFirm = () => {
const invalidateAuthStore = useInvalidateAuthStore()
@@ -9,11 +9,19 @@ export const CreateFirm = () => {
invalidateAuthStore().then();
}
const resourceBasePath = "hub/users";
const { onFinish } = useForm({
resource: `${resourceBasePath}/firms`,
action: "create",
redirect: "list",
onMutationSuccess: data => refreshUser()
});
return (
<CrudForm
schemaName={"FirmCreate"}
resource={"hub/users/firms/"}
onSuccess={() => { refreshUser() }}
schemaName={"Firm"}
resourceBasePath={resourceBasePath}
onSubmit={(data: any) => onFinish(data)}
/>
)
}