54 Commits

Author SHA1 Message Date
cb81e233a5 Correcting deprecated call on Menu comp 2025-05-07 14:56:47 +02:00
40648c3fdf Adding a link from Edit to List 2025-05-06 23:08:37 +02:00
6248248f0e Moving list schemas logique to crud lib 2025-05-06 22:14:41 +02:00
7bbd607376 Stop redirecting to login 2025-05-06 21:23:06 +02:00
4f5d5425fc Centralizing data provider error management 2025-05-06 21:19:32 +02:00
d48edbbf5f New way of handling firm initialization 2025-05-06 21:18:07 +02:00
0d337849c7 Updating column size 2025-05-06 21:16:52 +02:00
717a0ed830 Catching http error 400 whatever their formats 2025-05-06 21:16:26 +02:00
990e7fa226 Throwing 404 to the wall 2025-05-06 21:15:02 +02:00
5a8050145d Adding the 25 option to list size 2025-05-06 21:11:36 +02:00
1cc6e1e85d Adding types au List Schemas 2025-05-06 21:10:37 +02:00
765c0749bb Updating oauth validation redirect path 2025-05-06 21:07:57 +02:00
5080e5fdde Changing the order of firm and user validation 2025-05-06 21:07:25 +02:00
2fed7fa4e7 Dynamics list columns with a lot of work ahead 2025-05-06 00:58:47 +02:00
0613efa846 Correcting label update on updating 2025-05-04 17:30:28 +02:00
c8466c557d Adding filter on Entity type 2025-05-04 17:30:08 +02:00
0d7cad945c Handling order by by determinant fields 2025-05-04 16:26:53 +02:00
ea5093f2c2 Handling mongo indexes in firm 2025-05-04 02:26:06 +02:00
b542fd40a6 Renaming Collection CurrentFirmModel to CurrentFirm 2025-05-04 00:26:22 +02:00
a9e9f97c14 Phasing out Pydantic v1's class Config 2025-05-03 22:28:41 +02:00
90a46ada2d Finishing ForeignKey Migration and handling of their None values 2025-05-03 21:51:56 +02:00
4f0d943e04 Updating Foreign key to new CrudForm standard 2025-05-03 21:24:39 +02:00
04ff66f187 Improving display of numbered array fields 2025-05-03 21:05:35 +02:00
d28092874f Improving display of array item fields 2025-05-03 21:03:33 +02:00
d0e720f469 Correcting display of array fields 2025-05-03 21:03:09 +02:00
3dc91b329f Adding the "numbered" props to array field to display value position in array 2025-05-03 20:33:25 +02:00
2f2c5a035d Adding id as a metadata file come card ressource 2025-05-03 20:32:20 +02:00
32ce981d40 Customizing ArrayField template to control numbers of items per row (in a json schema stardardish way) 2025-05-03 19:56:27 +02:00
e7a4389fde Displaying Card for contract 2025-05-03 18:31:19 +02:00
f03f8374c8 Allowing crud forms with card format (showing read only fields) 2025-05-03 18:27:16 +02:00
78ffcb9b71 Allowing RichtextWidget to be readonly 2025-05-03 18:24:27 +02:00
0a657dca4b Removing meta fields from card ressource 2025-05-03 18:22:47 +02:00
8941d69ba4 Displacing non-standard string format to props 2025-05-03 18:21:37 +02:00
b8d9e8e804 Removing display related information in models 2025-05-03 18:20:31 +02:00
4bf414112a Adding canceled status to Contract -Not implemented yet- 2025-05-03 18:04:26 +02:00
6cc99812d2 Liste Translations 2025-05-02 17:39:38 +02:00
2b88e46ca6 Changing theme works for compatibility with mui localization 2025-05-02 16:40:42 +02:00
2c23992e52 Improve translation of dashboard and list 2025-05-02 11:59:10 +02:00
ba46c10449 Improving Cartouche 2025-05-02 11:45:45 +02:00
f878fa7886 Light reformating 2025-05-02 01:49:14 +02:00
ef43369425 Movinf card title from cartouche to edit 2025-05-02 00:56:51 +02:00
37193d2246 Implementing the partner map for author identifying 2025-05-02 00:51:05 +02:00
87f9119b0b Upgrading TipTap indent extension 2025-05-02 00:09:56 +02:00
f9b6aae927 Implementing the partner into the db and contracts 2025-05-02 00:03:08 +02:00
6683d60be5 Adding a read only resource schema 2025-05-01 22:43:40 +02:00
3942c54ad9 Translating Edit and New base page 2025-05-01 22:43:17 +02:00
0a22bc1b8f Implementing Contract creation on draft page 2025-05-01 22:42:40 +02:00
237f8d5742 Updating changePropertiesOrder method 2025-05-01 19:58:48 +02:00
8d72172e0a Adding a Cartouche for the card component 2025-05-01 19:44:19 +02:00
90aa5e06f2 Adding default value for corporation activity on Firm initialization 2025-05-01 19:43:22 +02:00
178f27cfe2 Moving Refine Form logic from crud lib to implementation 2025-05-01 19:42:39 +02:00
f4c6cdab3b More explicit schema provider error when resource is not found 2025-05-01 19:34:40 +02:00
40e20d0e64 Creating Card Resources that readonly non updatable resources fields 2025-04-30 00:12:52 +02:00
3a5a299b53 Upgrading gui packages 2025-04-29 23:58:26 +02:00
50 changed files with 1417 additions and 618 deletions

View File

@@ -4,10 +4,11 @@ from enum import Enum
from uuid import UUID
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict
from pydantic.json_schema import SkipJsonSchema
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey, \
CrudDocumentConfig
from firm.core.filter import Filter, FilterSchema
from firm.entity.models import Entity
@@ -17,6 +18,7 @@ class ContractStatus(str, Enum):
signed = 'signed'
printed = 'printed'
executed = 'executed'
canceled = 'canceled'
class ContractDraftStatus(str, Enum):
@@ -26,13 +28,13 @@ class ContractDraftStatus(str, Enum):
class DraftParty(BaseModel):
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Partie")
entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
part: str = Field(title="Rôle")
representative_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Représentant")
model_config = ConfigDict(title='Partie')
class Config:
title = 'Partie'
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
part: str = Field(title="Rôle")
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
class Party(BaseModel):
@@ -48,34 +50,31 @@ class ContractProvisionType(Enum):
template = 'template'
class ProvisionGenuine(BaseModel):
model_config = ConfigDict(title='Clause personalisée')
type: Literal['genuine'] = ContractProvisionType.genuine
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
class Config:
title = 'Clause personalisée'
class ContractProvisionTemplateReference(BaseModel):
model_config = ConfigDict(title='Template de clause')
type: Literal['template'] = ContractProvisionType.template
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"ProvisionTemplate",
displayed_fields=['title', 'body'],
props={"parametrized": True},
default="",
default=None,
title="Template de clause"
)
class Config:
title = 'Template de clause'
class DraftProvision(BaseModel):
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
model_config = ConfigDict(title='Clause')
class Config:
title = 'Clause'
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Provision(BaseModel):
@@ -90,34 +89,16 @@ class ContractDraft(CrudDocument):
"""
Brouillon de contrat à remplir
"""
model_config = ConfigDict(title='Brouillon de contrat')
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[DraftParty] = Field(title="Parties")
provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
title='Variables'
)
provisions: List[DraftProvision] = Field(title='Clauses')
variables: List[DictionaryEntry] = Field(default=[], title='Variables')
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
todo: List[str] = Field(default=[], title="Reste à faire")
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class Config:
title = 'Brouillon de contrat'
async def check_is_ready(self, db):
if self.status == ContractDraftStatus.published:
return
@@ -161,11 +142,16 @@ class Contract(CrudDocument):
Contrat publié. Les contrats ne peuvent pas être modifiés.
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
"""
model_config = ConfigDict(title='Contrat')
document_config = CrudDocumentConfig(
indexes=["parties.signature_uuid"],
)
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[Party] = Field(title="Parties")
provisions: List[Provision] = Field(
props={"items-per-row": "1", "numbered": True},
props={"items_per_row": "1", "numbered": True},
title='Clauses'
)
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
@@ -181,15 +167,6 @@ class Contract(CrudDocument):
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
return contract_label
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
@classmethod
async def find_by_signature_id(cls, db, signature_id: UUID):
request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}

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 pydantic import BaseModel, Field
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
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):
@@ -19,12 +20,12 @@ class ContractDraftCreate(Writer):
title: str = Field(title='Titre')
parties: List[DraftParty] = Field(title='Parties')
provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
props={"items_per_row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
props={"display": "dictionary"},
title='Variables'
)
@@ -41,34 +42,34 @@ class ContractDraftUpdate(ContractDraftCreate):
class ForeignEntityRead(BaseModel):
label: str
model_config = ConfigDict(title='Avocat')
class Config:
title = "Avocat"
label: str
class PartyRead(BaseModel):
model_config = ConfigDict(title='Partie')
signature_affixed: bool = Field(title='Signature apposée?')
signature_uuid: str = Field(format="signature-link", title="Lien vers signature")
signature_uuid: str = Field(props={"display": "signature-link"}, title="Lien vers signature")
part: str = Field(title='Rôle')
entity: ForeignEntityRead = Field(title='Client')
class Config:
title = "Partie"
class ContractRead(Reader, Contract):
parties: List[PartyRead]
lawyer: ForeignEntityRead
model_config = ConfigDict(title='Contrat')
class Config:
title = "Contrat"
parties: List[PartyRead] = Field(
props={"items_per_row": "2"},
title='Parties'
)
lawyer: ForeignEntityRead
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,51 +1,70 @@
from fastapi import HTTPException, Depends
from hub.auth import get_current_user
from firm.current_firm import CurrentFirm, 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}"]
self.instance = instance
self.firm = firm
self.current_firm = CurrentFirmModel.get_current(self.db)
self.current_firm = CurrentFirm.get_current(self.db)
def set_user(self, user):
def check_user(self, user):
for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm:
self.user = user
self.db.user = user
return
return True
raise PermissionError
async def set_user(self, user):
self.check_user(user)
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.partner = partner_entity
self.db.partner = partner_entity
return
raise PermissionError
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
registry = Registry(db_client, instance, firm)
if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
return registry
def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry:
async def get_authed_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
try:
registry.set_user(user)
registry.check_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
await registry.set_user(user)
return registry
async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
try:
registry.set_user(user)
registry.check_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
await registry.set_user(user)
return registry

View File

@@ -1,7 +1,7 @@
from collections import defaultdict
from collections.abc import Callable, Mapping
from typing import Any, Optional, Union
from pydantic import ValidationInfo, field_validator
from typing import Any, Optional, Union
from fastapi_filter.base.filter import BaseFilterModel
@@ -24,28 +24,6 @@ _odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str
class Filter(BaseFilterModel):
"""Base filter for beanie related filters.
Example:
```python
class MyModel:
id: PrimaryKey()
name: StringField(null=True)
count: IntField()
created_at: DatetimeField()
class MyModelFilter(Filter):
id: Optional[int]
id__in: Optional[str]
count: Optional[int]
count__lte: Optional[int]
created_at__gt: Optional[datetime]
name__ne: Optional[str]
name__nin: Optional[list[str]]
name__isnull: Optional[bool]
```
"""
def sort(self):
if not self.ordering_values:
return None
@@ -130,6 +108,51 @@ class Filter(BaseFilterModel):
query[field_name] = value
return query
@staticmethod
def field_exists(model, field_path: str) -> bool:
if "." in field_path:
[root, field] = field_path.split(".", 1)
return hasattr(model, "model_fields") and root in model.model_fields \
and model.model_fields[root].discriminator == field
return hasattr(model, field_path) or (hasattr(model, "model_fields") and field_path in model.model_fields)
@field_validator("*", mode="before", check_fields=False)
def validate_order_by(cls, value, field: ValidationInfo):
if field.field_name != cls.Constants.ordering_field_name:
return value
if not value:
return None
field_name_usages = defaultdict(list)
duplicated_field_names = set()
for field_name_with_direction in value:
field_name = field_name_with_direction.replace("-", "").replace("+", "")
if not cls.field_exists(cls.Constants.model, field_name):
raise ValueError(f"{field_name} is not a valid ordering field.")
field_name_usages[field_name].append(field_name_with_direction)
if len(field_name_usages[field_name]) > 1:
duplicated_field_names.add(field_name)
if duplicated_field_names:
ambiguous_field_names = ", ".join(
[
field_name_with_direction
for field_name in sorted(duplicated_field_names)
for field_name_with_direction in field_name_usages[field_name]
]
)
raise ValueError(
f"Field names can appear at most once for {cls.Constants.ordering_field_name}. "
f"The following was ambiguous: {ambiguous_field_names}."
)
return value
class FilterSchema(Filter):
label__ilike: Optional[str] = None
order_by: Optional[list[str]] = None

View File

@@ -1,12 +1,18 @@
from datetime import datetime, UTC
from typing import Optional
from typing import Optional, TypedDict, ClassVar
from beanie import PydanticObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pydantic import BaseModel, Field, computed_field
class CrudDocumentConfig(TypedDict, total=False):
fulltext_search: list[str]
indexes: list[str]
class CrudDocument(BaseModel):
document_config: ClassVar[CrudDocumentConfig] = CrudDocumentConfig()
id: Optional[PydanticObjectId] = Field(default=None)
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
created_by: Optional[PydanticObjectId] = Field(default=None, title="Créé par")
@@ -17,16 +23,13 @@ class CrudDocument(BaseModel):
def _id(self):
return self.id
@computed_field
@computed_field(title="Label")
def label(self) -> str:
return self.compute_label()
def compute_label(self) -> str:
return ""
class Settings:
fulltext_search = []
@classmethod
def _collection_name(cls):
return cls.__name__
@@ -35,9 +38,13 @@ class CrudDocument(BaseModel):
def _get_collection(cls, db) -> AsyncIOMotorCollection:
return db.get_collection(cls._collection_name())
@classmethod
def create_index(cls, db, index):
cls._get_collection(db).create_index(index)
@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 +59,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,20 +78,22 @@ 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, "updated_at": datetime.now(UTC)}
update_query = {
"$set": {field: value for field, value in model_dict.items() if field!= "id" }
}
await cls._get_collection(db).update_one({"_id": model.id}, update_query)
return await cls.get(db, model.id)
new_model = await cls.get(db, model.id)
if new_model.label != model.label:
await cls._get_collection(db).update_one({"_id": model.id}, {"$set": {"label": new_model.label}})
return new_model
@classmethod
async def delete(cls, db, model):
await cls._get_collection(db).delete_one({"_id": model.id})
def text_area(*args, **kwargs):
kwargs['widget'] = {
"formlyConfig": {

View File

@@ -3,12 +3,11 @@ from typing import Any
from beanie import PydanticObjectId
from pydantic import Field
from firm.core.models import CrudDocument
from firm.core.models import CrudDocument, CrudDocumentConfig
from firm.core.schemas import Writer, Reader
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
class CurrentFirmModel(CrudDocument):
class CurrentFirm(CrudDocument):
instance: str = Field()
firm: str = Field()
entity_id: PydanticObjectId = Field()
@@ -36,14 +35,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):
@@ -59,6 +59,10 @@ class CurrentFirmSchemaUpdate(Writer):
pass
class Partner(CrudDocument):
document_config = CrudDocumentConfig(
indexes=["user_id", "entity_id"],
)
user_id: PydanticObjectId = Field()
entity_id: PydanticObjectId = Field()

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
from firm.current_firm import CurrentFirmModel, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.current_firm import CurrentFirm, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.entity.models import Entity, Employee
from firm.entity.schemas import EntityRead
@@ -9,11 +9,18 @@ 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)
document = await CurrentFirm.get_current(reg.db)
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:
@@ -24,7 +31,7 @@ async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
corp = await Entity.create(reg.db, corporation_schema)
document = await CurrentFirmModel.create(reg.db, CurrentFirmModel(
document = await CurrentFirm.create(reg.db, CurrentFirm(
instance=reg.instance,
firm=reg.firm,
entity_id=corp.id,
@@ -35,6 +42,6 @@ async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_
@current_firm_router.put("/", response_description=f"Current Firm record updated")
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
document = await CurrentFirmModel.get_current(reg.db)
document = await CurrentFirmModel.update(reg.db, document, schema)
document = await CurrentFirm.get_current(reg.db)
document = await CurrentFirm.update(reg.db, document, schema)
return CurrentFirmSchemaRead.from_model(document)

View File

@@ -11,9 +11,6 @@ client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
async def init_db():
pass
async def stop_db():
client.close()

View File

@@ -1,10 +1,12 @@
from datetime import date, datetime
from datetime import date
from enum import Enum
from typing import List, Literal, Optional
from pydantic import Field, BaseModel
from beanie import Indexed, PydanticObjectId
from fastapi_filter import FilterDepends, with_prefix
from pydantic import Field, BaseModel, ConfigDict
from beanie import PydanticObjectId
from firm.core.models import CrudDocument
from firm.core.models import CrudDocument, ForeignKey, CrudDocumentConfig
from firm.core.filter import Filter, FilterSchema
@@ -13,15 +15,21 @@ class EntityType(BaseModel):
def label(self) -> str:
return self.title
class EntityTypeEnum(str, Enum):
individual = 'individual'
corporation = 'corporation'
institution = 'institution'
class Individual(EntityType):
model_config = ConfigDict(title='Particulier')
type: Literal['individual'] = 'individual'
firstname: Indexed(str) = Field(title='Prénom')
middlename: Indexed(str) = Field(default="", title='Autres prénoms')
lastname: Indexed(str) = Field(title='Nom de famille')
surnames: List[Indexed(str)] = Field(
firstname: str = Field(title='Prénom')
middlename: str = Field(default="", title='Autres prénoms')
lastname: str = Field(title='Nom de famille')
surnames: List[str] = Field(
default=[],
props={"items-per-row": "4", "numbered": True},
props={"items_per_row": "4", "numbered": True},
title="Surnoms"
)
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
@@ -30,51 +38,41 @@ class Individual(EntityType):
@property
def label(self) -> str:
# if len(self.surnames) > 0:
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
# return f"{self.firstname} \"{self.surnames[0]}\" {self.lastname}"
return f"{self.firstname} {self.lastname}"
class Config:
title = 'Particulier'
class Employee(BaseModel):
position: Indexed(str) = Field(title='Poste')
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
"condition": "entity_data.type=individual"
}
},
title='Employé'
)
model_config = ConfigDict(title='Fiche Employé')
class Config:
title = 'Fiche Employé'
position: str = Field(title='Poste')
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", title='Employé')
class Corporation(EntityType):
type: Literal['corporation'] = 'corporation'
title: Indexed(str) = Field(title='Dénomination sociale')
activity: Indexed(str) = Field(title='Activité')
employees: List[Employee] = Field(default=[], title='Employés')
model_config = ConfigDict(title='Entreprise')
class Config:
title = 'Entreprise'
type: Literal['corporation'] = 'corporation'
title: str = Field(title='Dénomination sociale')
activity: str = Field(title='Activité')
employees: List[Employee] = Field(default=[], title='Employés')
class Institution(Corporation):
type: Literal['institution'] = 'institution'
model_config = ConfigDict(title='Institution')
class Config:
title = 'Institution'
type: Literal['institution'] = 'institution'
class Entity(CrudDocument):
"""
Fiche d'un client
"""
model_config = ConfigDict(title='Client')
document_config = CrudDocumentConfig(
indexes=["entity_data.type"],
)
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: str = Field(default="", title='Adresse')
@@ -83,20 +81,17 @@ class Entity(CrudDocument):
return ""
return self.entity_data.label
class Settings(CrudDocument.Settings):
fulltext_search = ['label']
bson_encoders = {
date: lambda dt: dt if hasattr(dt, 'hour')
else datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class EntityDataFilter(Filter):
type__in: Optional[list[EntityTypeEnum]] = None
class Config:
title = 'Client'
class Constants(Filter.Constants):
model = EntityType
class EntityFilters(FilterSchema):
entity_data: Optional[EntityDataFilter] = FilterDepends(with_prefix("entity_data", EntityDataFilter))
class Constants(Filter.Constants):
model = Entity
search_model_fields = ["label"]

View File

@@ -1,4 +1,4 @@
from pydantic import Field
from pydantic import Field, ConfigDict
from firm.entity.models import Entity, Institution, Individual, Corporation
from firm.core.schemas import Writer, Reader
@@ -7,12 +7,11 @@ class EntityRead(Reader, Entity):
pass
class EntityCreate(Writer):
model_config = ConfigDict(title="Création d'un client")
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: str = Field(default="", title='Adresse')
class Config:
title = "Création d'un client"
class EntityIndividualCreate(EntityCreate):
entity_data: Individual

View File

@@ -0,0 +1,31 @@
import logging
from firm.contract.models import Contract, ContractDraft
from firm.core.depends import Registry
from firm.current_firm import CurrentFirm, Partner
from firm.db import client
from firm.entity.models import Entity
from firm.template.models import ContractTemplate, ProvisionTemplate
from hub.firm import Firm
collections = [CurrentFirm, Entity, Partner, Contract, ContractDraft, ContractTemplate, ProvisionTemplate]
logger = logging.getLogger('uvicorn.error')
async def create_documents_indexes(db):
for collection in collections:
if "indexes" in collection.document_config:
for field in collection.document_config["indexes"]:
collection.create_index(db, field)
async def init_all_db():
logger.info("[FRM] Creating index for firms")
for firm in await Firm.find({}).to_list():
reg = Registry(client, firm.instance, firm.firm)
await reg.current_firm
await create_documents_indexes(reg.db)
async def init_db():
await init_all_db()

View File

@@ -1,38 +1,19 @@
from typing import List
from typing import List, Optional
from html import unescape
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry, ForeignKey
from firm.core.filter import Filter, FilterSchema
class PartyTemplate(BaseModel):
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Partie"
)
part: str = Field(title="Rôle")
representative_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Représentant"
)
model_config = ConfigDict(title="Partie")
class Config:
title = 'Partie'
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
part: str = Field(title="Rôle")
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
def remove_html_tags(text):
@@ -46,6 +27,7 @@ class ProvisionTemplate(CrudDocument):
"""
Modèle de clause à décliner
"""
model_config = ConfigDict(title="Template de clause")
name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre")
@@ -54,57 +36,34 @@ class ProvisionTemplate(CrudDocument):
def compute_label(self) -> str:
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title', 'body']
class Config:
title = 'Template de clause'
class ProvisionTemplateReference(BaseModel):
provision_template_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "templates/provisions",
"schema": "ProvisionTemplate",
"displayedFields": ['title', 'body']
},
},
model_config = ConfigDict(title="Clause")
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"TemplateProvision",
['title', 'body'],
props={"parametrized": True},
title="Template de clause"
)
class Config:
title = 'Clause'
class ContractTemplate(CrudDocument):
"""
Modèle de contrat à décliner
"""
model_config = ConfigDict(title="Template de contrat")
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items-per-row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
title="Variables"
)
provisions: List[ProvisionTemplateReference] = Field(default=[], title="Clauses")
variables: List[DictionaryEntry] = Field(default=[], title="Variables")
def compute_label(self) -> str:
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
class Config:
title = 'Template de contrat'
class ContractTemplateFilters(FilterSchema):
class Constants(Filter.Constants):

View File

@@ -1,4 +1,4 @@
from pydantic import Field
from pydantic import Field, ConfigDict
from typing import List
from firm.template.models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry
@@ -11,24 +11,25 @@ class ContractTemplateRead(Reader, ContractTemplate):
class ContractTemplateCreate(Writer):
model_config = ConfigDict(title="Template de Contrat")
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[PartyTemplate] = Field(default=[], title="Parties")
parties: List[PartyTemplate] = Field(
default=[],
props={"items_per_row": "2"},
title="Parties")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items-per-row": "1", "numbered": True},
props={"items_per_row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
props={"required": False},
props={"display": "dictionary", "required": False},
title="Variables"
)
class Config:
title = 'Template de Contrat'
class ContractTemplateUpdate(ContractTemplateCreate):
pass
@@ -39,13 +40,12 @@ class ProvisionTemplateRead(Reader, ProvisionTemplate):
class ProvisionTemplateCreate(Writer):
model_config = ConfigDict(title="Template de Clause")
name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline(title="Corps")
class Config:
title = 'Template de Clause'
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
pass

View File

@@ -67,7 +67,7 @@ class AuthenticationBackendMe(AuthenticationBackend):
class CookieTransportOauth(CookieTransport):
async def get_login_response(self, token: str) -> Response:
response = RedirectResponse("/auth/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
return self._set_login_cookie(response, token)
@staticmethod

View File

@@ -5,7 +5,7 @@ from hub import hub_router
from hub.db import init_db as hub_init_db
from firm import firm_router
from firm.db import init_db as firm_init_db
from firm.init_db import init_db as firm_init_db
if __name__ == '__main__':

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",
@@ -42,6 +43,7 @@
"mui-tiptap": "^1.18.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.30.0",
"react-i18next": "^15.5.1",
"react-router": "^7.0.2"
@@ -2622,9 +2624,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
"cpu": [
"arm"
],
@@ -2636,9 +2638,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz",
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
"cpu": [
"arm64"
],
@@ -2650,9 +2652,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz",
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
"integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
"cpu": [
"arm64"
],
@@ -2664,9 +2666,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz",
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
"cpu": [
"x64"
],
@@ -2678,9 +2680,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz",
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
"cpu": [
"arm64"
],
@@ -2692,9 +2694,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz",
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
"cpu": [
"x64"
],
@@ -2706,9 +2708,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz",
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
"cpu": [
"arm"
],
@@ -2720,9 +2722,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz",
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
"cpu": [
"arm"
],
@@ -2734,9 +2736,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz",
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
"cpu": [
"arm64"
],
@@ -2748,9 +2750,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz",
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
"cpu": [
"arm64"
],
@@ -2762,9 +2764,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz",
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
"cpu": [
"loong64"
],
@@ -2776,9 +2778,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz",
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
"cpu": [
"ppc64"
],
@@ -2790,9 +2792,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz",
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
"cpu": [
"riscv64"
],
@@ -2804,9 +2806,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz",
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
"cpu": [
"riscv64"
],
@@ -2818,9 +2820,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz",
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
"cpu": [
"s390x"
],
@@ -2832,9 +2834,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz",
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
"integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
"cpu": [
"x64"
],
@@ -2846,9 +2848,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz",
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
"cpu": [
"x64"
],
@@ -2860,9 +2862,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz",
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
"cpu": [
"arm64"
],
@@ -2874,9 +2876,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz",
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
"cpu": [
"ia32"
],
@@ -2888,9 +2890,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz",
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
"cpu": [
"x64"
],
@@ -5035,9 +5037,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.142",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz",
"integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==",
"version": "1.5.144",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.144.tgz",
"integrity": "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -5978,9 +5980,9 @@
"license": "ISC"
},
"node_modules/flow-parser": {
"version": "0.268.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.268.0.tgz",
"integrity": "sha512-URZmPy/jKDDIJUHUfC+5KNwaPcfONTL3R8xltQWVEoCKLWowVebEBg89nbAnYHNo6ev8KzKWFpOROfHZdaCoxA==",
"version": "0.269.1",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.269.1.tgz",
"integrity": "sha512-2Yr0kqvT7RwaGL192nT78O5AWJeECQjl0NEzBkMsx8OJt63BvNl5yvSIbE4qZ1VDSjEkhbUgaWYdwX354bVNjw==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -6525,9 +6527,9 @@
}
},
"node_modules/i18next": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.1.tgz",
"integrity": "sha512-8S8PyZbrymJZn3DaN70/34JYWNhsqrU6yA4MuzcygJBv+41dgNMocEA8h+kV1P7MCc1ll03lOTOIXE7mpNCicw==",
"version": "25.0.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.2.tgz",
"integrity": "sha512-xWxgK8GAaPYkV9ia2tdgbtdM+qiC+ysVTBPvXhpCORU/+QkeQe3BSI7Crr+c4ZXULN1PfnXG/HY2n7HGx4KKBg==",
"funding": [
{
"type": "individual",
@@ -9179,6 +9181,18 @@
"react": "^18.3.1"
}
},
"node_modules/react-error-boundary": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-hook-form": {
"version": "7.56.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz",
@@ -9289,9 +9303,9 @@
}
},
"node_modules/react-router": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz",
"integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -9576,9 +9590,9 @@
}
},
"node_modules/rollup": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9592,26 +9606,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.40.0",
"@rollup/rollup-android-arm64": "4.40.0",
"@rollup/rollup-darwin-arm64": "4.40.0",
"@rollup/rollup-darwin-x64": "4.40.0",
"@rollup/rollup-freebsd-arm64": "4.40.0",
"@rollup/rollup-freebsd-x64": "4.40.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.0",
"@rollup/rollup-linux-arm-musleabihf": "4.40.0",
"@rollup/rollup-linux-arm64-gnu": "4.40.0",
"@rollup/rollup-linux-arm64-musl": "4.40.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.0",
"@rollup/rollup-linux-riscv64-gnu": "4.40.0",
"@rollup/rollup-linux-riscv64-musl": "4.40.0",
"@rollup/rollup-linux-s390x-gnu": "4.40.0",
"@rollup/rollup-linux-x64-gnu": "4.40.0",
"@rollup/rollup-linux-x64-musl": "4.40.0",
"@rollup/rollup-win32-arm64-msvc": "4.40.0",
"@rollup/rollup-win32-ia32-msvc": "4.40.0",
"@rollup/rollup-win32-x64-msvc": "4.40.0",
"@rollup/rollup-android-arm-eabi": "4.40.1",
"@rollup/rollup-android-arm64": "4.40.1",
"@rollup/rollup-darwin-arm64": "4.40.1",
"@rollup/rollup-darwin-x64": "4.40.1",
"@rollup/rollup-freebsd-arm64": "4.40.1",
"@rollup/rollup-freebsd-x64": "4.40.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
"@rollup/rollup-linux-arm-musleabihf": "4.40.1",
"@rollup/rollup-linux-arm64-gnu": "4.40.1",
"@rollup/rollup-linux-arm64-musl": "4.40.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
"@rollup/rollup-linux-riscv64-gnu": "4.40.1",
"@rollup/rollup-linux-riscv64-musl": "4.40.1",
"@rollup/rollup-linux-s390x-gnu": "4.40.1",
"@rollup/rollup-linux-x64-gnu": "4.40.1",
"@rollup/rollup-linux-x64-musl": "4.40.1",
"@rollup/rollup-win32-arm64-msvc": "4.40.1",
"@rollup/rollup-win32-ia32-msvc": "4.40.1",
"@rollup/rollup-win32-x64-msvc": "4.40.1",
"fsevents": "~2.3.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",
@@ -38,6 +39,7 @@
"mui-tiptap": "^1.18.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.30.0",
"react-i18next": "^15.5.1",
"react-router": "^7.0.2"

View File

@@ -171,6 +171,12 @@
}
},
"schemas": {
"created_by": "Created by",
"created_at": "Created at",
"updated_by": "Updated by",
"updated_at": "Updated at",
"label": "Label",
"type": "Type",
"individual": {
"type": "Individual",
"lastname": "Lastname",
@@ -203,13 +209,15 @@
"entity": {
"entity_data": "Informations",
"address": "Address",
"resource_title": "Entity"
"resource_title": "Entity",
"resource_plural": "Entities"
},
"provision_template": {
"name": "Name",
"title": "Title",
"body": "Body",
"resource_title": "Provision Template"
"resource_title": "Provision Template",
"resource_plural": "Provision Templates"
},
"contract_template": {
"name": "Name",
@@ -217,10 +225,11 @@
"provisions": "Provisions",
"parties": "Parties",
"variables": "Variables",
"resource_title": "Contract Template"
"resource_title": "Contract Template",
"resource_plural": "Contract Templates"
},
"party_template": {
"entity_id": "Party Template",
"entity_id": "Entity",
"representative_id": "Representative",
"part": "Part",
"resource_title": "Party"
@@ -240,7 +249,8 @@
"parties": "Parties",
"provisions": "Provisions",
"variables": "Variables",
"resource_title": "Contract Draft"
"resource_title": "Contract Draft",
"resource_plural": "Contract Drafts"
},
"draft_party": {
"entity_id": "Client",
@@ -267,6 +277,7 @@
"date": "Date",
"location": "Location",
"resource_title": "Contract",
"resource_plural": "Contracts",
"draft_id": "Draft"
}
}

View File

@@ -171,6 +171,12 @@
}
},
"schemas": {
"created_by": "Créé par",
"created_at": "Créé le",
"updated_by": "Modifié par",
"updated_at": "Modifié le",
"label": "Label",
"type": "Type",
"individual": {
"type": "Particulier",
"middlename": "Autres prénoms",
@@ -183,7 +189,7 @@
},
"corporation": {
"type": "Entreprise",
"title": "Titre",
"title": "Dénomination sociale",
"activity": "Activité",
"employees": "Employés",
"resource_title": "Entreprise"
@@ -203,13 +209,15 @@
"entity": {
"entity_data": "Informations",
"address": "Adresse",
"resource_title": "Entité"
"resource_title": "Entité",
"resource_plural": "Entités"
},
"provision_template": {
"name": "Nom",
"body": "Corps",
"title": "Titre",
"resource_title": "Template de Clause"
"resource_title": "Template de Clause",
"resource_plural": "Templates de Clauses"
},
"contract_template": {
"name": "Nom",
@@ -217,7 +225,8 @@
"parties": "Parties",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Template de Contrat"
"resource_title": "Template de Contrat",
"resource_plural": "Templates de Contrats"
},
"party_template": {
"entity_id": "Entité",
@@ -240,7 +249,8 @@
"title": "Titre",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Brouillon de Contrat"
"resource_title": "Brouillon de Contrat",
"resource_plural": "Brouillons de Contrats"
},
"draft_party": {
"part": "Rôle",
@@ -266,6 +276,7 @@
"contract": {
"draft_id": "Brouillon",
"resource_title": "Contrat",
"resource_plural": "Contrats",
"location": "Lieu",
"date": "Date"
}

View File

@@ -5,13 +5,8 @@ import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui"
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { ThemeProvider } from "@mui/material/styles";
import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
import routerBindings, {
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import authProvider from "./providers/auth-provider";
import dataProvider from "./providers/data-provider";
@@ -22,9 +17,9 @@ import { ForgotPassword } from "./components/auth/ForgotPassword";
import { UpdatePassword } from "./components/auth/UpdatePassword";
import { Header } from "./components";
import { I18nTheme } from "./components/I18nTheme";
import { HubRoutes } from "./pages/hub";
import { FirmRoutes } from "./pages/firm";
import rpcTheme from "./theme";
function App() {
const { t, i18n } = useTranslation();
@@ -37,47 +32,48 @@ function App() {
return (
<BrowserRouter>
<ThemeProvider theme={rpcTheme}>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
title: {
text: "Roleplay Contracts",
icon: <HistoryEduIcon />
},
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true,
reactQuery: {
clientConfig: {
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
if (error.statusCode >= 400 && error.statusCode <= 499) {
return false
}
return failureCount < 4
},
}
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
title: {
text: "Roleplay Contracts",
icon: <HistoryEduIcon />
},
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true,
reactQuery: {
clientConfig: {
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
const status = error.statusCode ? error.statusCode : error.status
if (status >= 400 && status<= 499) {
return false
}
return failureCount < 4
},
}
}
}
}}
>
}
}}
>
<I18nTheme>
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}>
<Authenticated key="authenticated-routes" fallback={<Login />}>
<Outlet />
</Authenticated>
)}
@@ -95,10 +91,10 @@ function App() {
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</ThemeProvider>
</I18nTheme>
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</BrowserRouter>
);
}

View File

@@ -0,0 +1,40 @@
import { useTranslation } from "@refinedev/core";
import { useContext } from "react";
import { FirmContext } from "../contexts/FirmContext";
import Grid2 from '@mui/material/Grid2';
type CartoucheProps = {
record: any
}
const Cartouche = (props: CartoucheProps) => {
const { record } = props;
const { translate: t } = useTranslation();
return (
<>
<Grid2 container spacing={0}>
<Grid2 size={2}>{t("schemas.created_by")}:</Grid2>
<Grid2 size={4}><AuthorField partnerId={record.created_by} /></Grid2>
<Grid2 size={2}>{t("schemas.created_at")}:</Grid2>
<Grid2 size={4}>{new Date(record.created_at).toLocaleString()}</Grid2>
<Grid2 size={2}>{t("schemas.updated_by")}:</Grid2>
<Grid2 size={4}><AuthorField partnerId={record.updated_by} /></Grid2>
<Grid2 size={2}>{t("schemas.updated_at")}:</Grid2>
<Grid2 size={4}>{new Date(record.updated_at).toLocaleString()}</Grid2>
</Grid2>
</>
)
}
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

@@ -0,0 +1,20 @@
import React, { PropsWithChildren } from "react";
import { useTranslation } from "@refinedev/core";
import { useTheme } from "@mui/material";
import * as locales from '@mui/material/locale';
import { createTheme, ThemeProvider } from "@mui/material/styles";
type SupportedLocales = keyof typeof locales;
export const I18nTheme: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { getLocale } = useTranslation();
const theme = useTheme()
const themeWithLocale = createTheme(theme, locales[getLocale() as SupportedLocales])
return (
<ThemeProvider theme={themeWithLocale}>
{ children }
</ThemeProvider>
);
}

View File

@@ -117,8 +117,8 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
anchorEl={anchorEl}
open={openUserMenu}
onClose={handleCloseUserMenu}
MenuListProps={{
'aria-labelledby': 'user-menu-button',
slotProps={{
list:{ 'aria-labelledby': 'user-menu-button' }
}}
>
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>

View File

@@ -1,9 +1,14 @@
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";
import { FirmInitForm } from "../pages/firm";
import { Header } from "../components";
type FirmContextType = {
currentFirm: IFirm,
partnerMap?: Map<string, string>
}
export const FirmContext = createContext<FirmContextType>(
@@ -12,13 +17,35 @@ 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"
throw({statusCode: 400});
}
const currentFirm: IFirm = { instance, firm }
if (isLoading) {
return <CircularProgress />
}
if (isError && error) {
if (error.statusCode == 405) {
return <><Header /><FirmInitForm currentFirm={currentFirm} /></>
}
if (error.statusCode == 404) {
throw error;
}
}
currentFirm.entity = data?.data.entity;
let value: FirmContextType = {
currentFirm: currentFirm,
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

@@ -8,14 +8,14 @@ i18n
.use(detector)
.use(initReactI18next)
.init({
supportedLngs: ["EN", "FR"],
supportedLngs: ["enUS", "frFR"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
},
//saveMissing: true,
ns: ["common"],
defaultNS: "common",
fallbackLng: ["EN", "FR"],
fallbackLng: ["enUS", "frFR"],
});
export default i18n;

View File

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

View File

@@ -3,6 +3,8 @@ import Form from "@rjsf/mui";
import { RegistryFieldsType, RegistryWidgetsType, RJSFSchema, UiSchema } from "@rjsf/utils";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
import ArrayFieldTemplate from "./templates/ArrayFieldTemplate"
import ArrayFieldItemTemplate from "./templates/ArrayFieldItemTemplate";
import { ResourceContext } from "../contexts/ResourceContext";
import { ReactNode } from "react";
@@ -24,6 +26,11 @@ export const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
const customTemplates = {
ArrayFieldTemplate,
ArrayFieldItemTemplate
}
export const BaseForm: React.FC<BaseFormProps> = (props) => {
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
@@ -38,6 +45,7 @@ export const BaseForm: React.FC<BaseFormProps> = (props) => {
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
templates={customTemplates}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children}
/>

View File

@@ -8,32 +8,32 @@ 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
card?: boolean
}
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=(data: any) => {}, card=false } = props;
const [schema, setSchema] = useState({});
const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const schemaFullName = id === undefined ? `${schemaName}Create` : `${schemaName}Update`;
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaFullName);
let resourceSchema
if (record === undefined) {
resourceSchema = await jsonschemaProvider.getCreateResourceSchema(schemaName);
} else {
if (card) {
resourceSchema = await jsonschemaProvider.getCardResourceSchema(schemaName);
} else {
resourceSchema = await jsonschemaProvider.getUpdateResourceSchema(schemaName);
}
}
setSchema(resourceSchema);
setSchemaLoading(false);
} catch (error) {
@@ -44,19 +44,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

@@ -0,0 +1,95 @@
import { DataGrid, GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
import { UiSchema } from "@rjsf/utils";
import React, { useEffect, useState } from "react";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { CircularProgress } from "@mui/material";
type CrudListProps = {
schemaName: string,
uiSchema?: UiSchema,
columnDefinitions: ColumnDefinition[],
dataGridProps: any,
resourceBasePath: string,
onRowClick?: (params: any) => void
}
type ColumnSchema<T extends GridValidRowModel> = {
columns: GridColDef<T>[],
columnVisibilityModel: GridColumnVisibilityModel
}
type ColumnDefinition = {
field: string,
column: Partial<GridColDef>,
hide?: boolean
}
const CrudList = <T extends GridValidRowModel>(props: CrudListProps) => {
const {
dataGridProps,
onRowClick,
schemaName,
columnDefinitions
} = props;
const [columnSchema, setColumnSchema] = useState<ColumnSchema<T>>()
const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
const definedColumns = computeColumnSchema<T>(columnDefinitions, resourceColumns)
setColumnSchema(definedColumns);
console.log(resourceColumns);
setSchemaLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setSchemaLoading(false);
}
};
fetchSchema();
}, []);
if (schemaLoading || columnSchema === undefined) {
return <CircularProgress />
}
return (
<DataGrid
{...dataGridProps}
columns={columnSchema.columns}
onRowClick={onRowClick}
pageSizeOptions={[10, 15, 25, 50, 100]}
disableColumnFilter={true}
initialState={{
columns: {
columnVisibilityModel: columnSchema.columnVisibilityModel
}
}}
/>
)
}
function computeColumnSchema<T extends GridValidRowModel>(definitionColumns: ColumnDefinition[], resourceColumns: GridColDef[]): ColumnSchema<T> {
//reorder resourceColumns as in definition
definitionColumns.slice().reverse().forEach(first => {
resourceColumns.sort(function(x,y){ return x.field == first.field ? -1 : y.field == first.field ? 1 : 0; });
})
let visibilityModel: GridColumnVisibilityModel = {}
resourceColumns.forEach((resource, index) =>{
visibilityModel[resource.field] = definitionColumns.some(col => col.field == resource.field && !col.hide)
definitionColumns.forEach((def) => {
if (def.field == resource.field) {
resourceColumns[index] = {...resource, ...def.column};
}
})
})
return {
columns: resourceColumns,
columnVisibilityModel: visibilityModel
}
}
export default CrudList;

View File

@@ -0,0 +1,96 @@
import { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Grid2';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
import Stack from "@mui/material/Stack";
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldItemTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateItemType<T, S, F>) {
const {
children,
disabled,
hasToolbar,
hasCopy,
hasMoveDown,
hasMoveUp,
hasRemove,
index,
onCopyIndexClick,
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
} = props;
const { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } = registry.templates.ButtonTemplates;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
const displayToolbar = hasToolbar && !props.readonly;
return (
<Grid2 container alignItems='center'>
<Grid2 style={{ overflow: 'auto' }} size={ displayToolbar ? 11 : 12}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid2>
{displayToolbar && (
<Grid2 size={1}>
<Stack direction="column">
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasCopy && (
<CopyButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onCopyIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</Stack>
</Grid2>
)}
</Grid2>
);
}

View File

@@ -0,0 +1,105 @@
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Grid2';
import Paper from '@mui/material/Paper';
import {
getTemplate,
getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
} from '@rjsf/utils';
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends CrudTextRJSFSchema = CrudTextRJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateProps<T, S, F>) {
const { canAdd, disabled, idSchema, uiSchema, items, onAddClick, readonly, registry, required, schema, title } =
props;
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T, S, F>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T, S, F>(
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
let gridSize = 12;
let numbered = false
if (schema.props) {
if (schema.props.hasOwnProperty("items_per_row")) {
gridSize = gridSize / schema.props.items_per_row;
}
if (schema.props.hasOwnProperty("numbered")) {
numbered = schema.props.numbered;
}
}
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<Grid2 container justifyContent='flex-start'>
{items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>, index) => (
<Grid2 key={key} size={gridSize} >
<Grid2 container sx={{alignItems: "center"}} >
{numbered &&<Grid2 size={.5} ><Typography variant="h4">{index + 1}</Typography></Grid2>}
<Grid2 size={numbered ? 11.5 : 12} ><ArrayFieldItemTemplate key={key} {...itemProps} /></Grid2>
</Grid2>
</Grid2>
))}
</Grid2>
{canAdd && (
<Grid2 container justifyContent='flex-end'>
<Grid2>
<Box mt={2}>
<AddButton
className='array-item-add'
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
</Box>
</Grid2>
</Grid2>
)}
</Box>
</Paper>
);
}

View File

@@ -6,7 +6,7 @@ import ClearIcon from '@mui/icons-material/Clear';
import EditIcon from '@mui/icons-material/Edit';
import NoteAddIcon from '@mui/icons-material/NoteAdd';
import React, { useState, useEffect, useContext, Fragment } from "react";
import { useList, useOne } from "@refinedev/core";
import { useForm, useList, useOne } from "@refinedev/core";
import { ResourceContext } from "../../contexts/ResourceContext";
import { CrudForm } from "../crud-form";
@@ -103,7 +103,7 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainer
<FormContainerNew
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
@@ -166,7 +166,7 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainer
<FormContainerEdit
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
@@ -203,11 +203,49 @@ type FormContainerProps = {
onSuccess: (data: any) => void
}
const FormContainer = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
const FormContainerEdit = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id, onSuccess } = props;
const { onFinish, query, formLoading } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "edit",
id: id,
});
if (formLoading || query?.data === undefined) {
return <CircularProgress />
}
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={(data) => onSuccess(data)} />
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
record={query.data.data}
onSubmit={(data:any) => {
onFinish(data);
onSuccess(data);
}} />
</Box>
)
}
const FormContainerNew = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, onSuccess } = props;
const { onFinish } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "create"
});
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
onSubmit={(data:any) => {
onFinish(data);
onSuccess(data);
}} />
</Box>
)
}

View File

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

View File

@@ -57,7 +57,7 @@ const StyledLabelledOutlined = styled(LabelledOutlined)(({ theme }) => [{
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
const { schema, value, onChange, label, id } = props;
const { schema, value, onChange, label, id, readonly } = props;
const isMultiline = schema.props.multiline === true;
let editorOptions: UseEditorOptions;
@@ -92,14 +92,15 @@ const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSche
<TextContainer>
<RichTextEditorProvider editor={editor}>
<TableBubbleMenu />
<RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>
{!readonly && <RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>}
{readonly && <RichTextField variant="standard" disabled={true}/>}
</RichTextEditorProvider>
</TextContainer>
<RightContainer>&nbsp;</RightContainer>

View File

@@ -1,22 +1,163 @@
import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n'
import { JSONSchema7Definition } from "json-schema";
import { GridColDef } from "@mui/x-data-grid";
import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType";
const API_URL = "/api/v1";
type CrudRJSFSchema = RJSFSchema & {
properties?: {
[key: string]: JSONSchema7Definition & {
readOnly?: boolean | undefined;
};
} | undefined;
}
const meta_fields = ["id", "label", "created_at", "created_by", "updated_at", "updated_by"]
export const jsonschemaProvider = {
getResourceSchema: async (resourceName: string): Promise<RJSFSchema> => {
return buildResource(await getJsonschema(), resourceName)
}
};
getCardResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
const updateSchema = await getResourceSchema(`${resourceName}Update`);
const readSchema = await getResourceSchema(`${resourceName}Read`);
let rawSchema: RJSFSchema;
const getJsonschema = async (): Promise<RJSFSchema> => {
if (rawSchema === undefined) {
const response = await fetch(`${API_URL}/openapi.json`,);
rawSchema = await response.json();
for (let prop_name in readSchema.properties) {
if (meta_fields.indexOf(prop_name) > -1) {
delete readSchema.properties[prop_name];
} else if (! updateSchema.hasOwnProperty(prop_name)) {
if (is_reference(readSchema.properties[prop_name])) {
let subresourceName = get_reference_name(readSchema.properties[prop_name]);
readSchema.components.schemas[subresourceName].readOnly = true;
} else {
readSchema.properties[prop_name].readOnly = true;
}
}
}
changePropertiesOrder(readSchema);
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
},
getReadResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Read`)
},
getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Update`)
},
getCreateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Create`)
},
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
return getColumns(`${resourceName}Read`)
},
}
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
return buildColumns(await getJsonschema(), resourceName)
}
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
if (rawSchemas.components.schemas[resourceName] === undefined) {
throw new Error(`Resource "${resourceName}" not found in schema.`);
}
return rawSchema;
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
let resource = rawSchemas.components.schemas[resourceName];
let result: GridColDef[] = [];
for (const prop_name in resource.properties) {
let prop = resource.properties[prop_name];
if (is_reference(prop)) {
const subresourceName = get_reference_name(prop);
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
} else if (is_union(prop)) {
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
let seen = new Set<string>();
for (let i in union) {
if (is_reference(union[i])) {
const subresourceName = get_reference_name(union[i]);
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
for (const s of subcolumns) {
if (! seen.has(s.field)) {
result.push(s);
seen.add(s.field);
}
}
}
}
} else if (is_enum(prop)) {
let seen = new Set<string>();
for (let i in prop.allOf) {
if (is_reference(prop.allOf[i])) {
const subresourceName = get_reference_name(prop.allOf[i]);
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
for (const s of subcolumns) {
if (! seen.has(s.field)) {
result.push(s);
seen.add(s.field);
}
}
}
}
} else if (is_array(prop) && is_reference(prop.items)) {
const subresourceName = get_reference_name(prop.items);
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
} else {
let valueGetter: undefined|((value: any, row: any) => any) = undefined;
let type: GridColType = "string";
if (is_array(prop)) {
valueGetter = (value: any[], row: any ) => {
return value.concat(".");
}
} else if (prefix !== undefined) {
valueGetter = (value: any, row: any ) => {
let parent = row;
for (const col of prefix.split(".")) {
parent = parent[col];
}
return parent ? parent[prop_name] : "";
}
} else {
if (prop.type == "string" && prop.format == "date-time") {
type = "dateTime"
valueGetter = (value: string) => new Date(value)
}
}
if (prop.type == "string" && prop.format == "date-time") {
}
const column: GridColDef = {
field: prefix ? `${prefix}.${prop_name}` : prop_name,
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
type: type,
valueGetter: valueGetter
}
result.push(column);
}
}
return result;
}
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
return buildResource(await getJsonschema(), resourceName)
}
function convertCamelToSnake(str: string): string {
@@ -24,10 +165,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]);
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
@@ -62,6 +205,15 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
return resource;
}
let rawSchema: RJSFSchema;
const getJsonschema = async (): Promise<RJSFSchema> => {
if (rawSchema === undefined) {
const response = await fetch(`${API_URL}/openapi.json`,);
rawSchema = await response.json();
}
return rawSchema;
}
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
const subresourceName = get_reference_name(prop_reference);
const subresource = buildResource(rawSchemas, subresourceName);
@@ -75,23 +227,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

@@ -0,0 +1,4 @@
export const Error404Page = () => {
return <h2>EROR NO FUND</h2>
};

View File

@@ -1,9 +1,14 @@
import { Route, Routes } from "react-router";
import { useContext } from "react";
import { Route, Routes, useParams } from "react-router";
import { useOne, useTranslation } from "@refinedev/core";
import { DeleteButton } from "@refinedev/mui";
import { CircularProgress, Stack } from "@mui/material";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { FirmContext } from "../../contexts/FirmContext";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
import Cartouche from "../../components/Cartouche";
type Contract = {
export type Contract = {
id: string,
label: string
}
@@ -13,22 +18,51 @@ export const ContractRoutes = () => {
<Routes>
<Route index element={ <ListContract /> } />
<Route path="/edit/:record_id" element={ <EditContract /> } />
<Route path="/create" element={ <CreateContract /> } />
</Routes>
);
}
const ListContract = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "label", column: { flex: 1 }},
];
return <List<Contract> resource={`contracts`} columns={columns} />
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
}
const EditContract = () => {
return <Edit<Contract> resource={`contracts`} schemaName={"Contract"} />
}
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { record_id } = useParams();
const CreateContract = () => {
return <New<Contract> resource={`contracts`} schemaName={"Contract"} />
}
const { data, isLoading } = useOne({resource: `${resourceBasePath}/contracts`, id: record_id,});
if (isLoading || data?.data === undefined) {
return <CircularProgress />
}
const record = data.data;
return (
<>
<h2>{record.label}</h2>
<Cartouche record={record}/>
<CrudForm
resourceBasePath={resourceBasePath}
schemaName={"Contract"}
uiSchema={{"ui:readonly": true }}
record={record}
card={true}
>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "flex-end",
alignItems: "center",
}}>
{ record.status == "published" && (<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>) }
</Stack>
</CrudForm>
</>
)
}

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,
@@ -27,13 +29,69 @@ export const DraftRoutes = () => {
const ListDraft = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "label", column: { flex: 1 }},
];
return <List<Draft> resource={`contracts/drafts`} columns={columns} />
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
}
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

@@ -22,10 +22,11 @@ export const EntityRoutes = () => {
const ListEntity = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "entity_data", headerName: "Type", flex: 1, valueFormatter: ({ type }: {type: string}) => type }
{ field: "entity_data.type", column: { width: 110 }},
{ field: "label", column: { flex: 1 }},
{ field: "updated_at", column: { width: 160 }},
];
return <List<Entity> resource={`entities`} columns={columns} />
return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} />
}
const EditEntity = () => {

View File

@@ -21,9 +21,9 @@ export const ProvisionRoutes = () => {
const ListProvision = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "label", column: { flex: 1 }},
];
return <List<Provision> resource={`templates/provisions`} columns={columns} />
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
}
const EditProvision = () => {

View File

@@ -20,9 +20,9 @@ export const TemplateRoutes = () => {
const ListTemplate = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "label", column: { flex: 1 }},
];
return <List<Template> resource={`templates/contracts`} columns={columns} />
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
}
const EditTemplate = () => {

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, Link } 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,43 @@ 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="../" />
}
if (query.error?.status == 404) {
throw query.error
}
const record = query.data.data;
return (
<>
<Link to={"../"} >
<Button>{t("buttons.list")}</Button>
</Link>
<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 +64,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,44 +1,53 @@
import { UiSchema } from "@rjsf/utils";
import { List as RefineList, useDataGrid } from "@refinedev/mui";
import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid";
import { Link, useNavigate } from "react-router"
import React, { useContext } from "react";
import { Link, useNavigate } from "react-router"
import { UiSchema } from "@rjsf/utils";
import { useTranslation } from "@refinedev/core";
import { List as RefineList, useDataGrid } from "@refinedev/mui";
import { Button } from "@mui/material";
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
import { FirmContext } from "../../../contexts/FirmContext";
import CrudList from "../../../lib/crud/components/crud-list";
type ListProps<T extends GridValidRowModel> = {
type ListProps = {
resource: string,
columns: GridColDef<T>[],
schemaName?: string,
columns: ColumnDefinition[],
schemaName: string,
uiSchema?: UiSchema,
}
const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
const { resource, columns } = props;
type ColumnDefinition = {
field: string,
column: Partial<GridColDef>,
hide?: boolean
}
const List = <T extends GridValidRowModel>(props: ListProps) => {
const { resource, columns, schemaName } = props;
const { translate: t } = useTranslation();
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { dataGridProps } = useDataGrid<T>({resource: `${resourceBasePath}/${resource}`});
const { dataGridProps, tableQueryResult } = useDataGrid<T>({
resource: `${resourceBasePath}/${resource}`,
});
const navigate = useNavigate();
const cols = React.useMemo<GridColDef<T>[]>(
() => columns,
[],
);
const handleRowClick = (params: any, event: any) => {
navigate(`edit/${params.id}`)
if (tableQueryResult.error?.status == 404) {
throw tableQueryResult.error
}
return (
<RefineList>
<Link to={"create"} >
<Button>Create</Button>
<Button>{t("buttons.create")}</Button>
</Link>
<DataGrid
{...dataGridProps}
columns={cols}
onRowClick={handleRowClick} />
<CrudList
dataGridProps={dataGridProps}
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
schemaName={schemaName}
resourceBasePath={resourceBasePath}
columnDefinitions={columns}
/>
</RefineList>
)
}

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 { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
import React from "react";
import { useForm, useTranslation } from "@refinedev/core";
import { 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";
@@ -10,51 +10,44 @@ import { ContractRoutes } from "./ContractRoutes";
import { DraftRoutes } from "./DraftRoutes";
import { TemplateRoutes } from "./TemplateRoutes";
import { ProvisionRoutes } from "./ProvisionRoutes";
import { ErrorBoundary } from "react-error-boundary";
import { Error404Page } from "../ErrorPage";
export const FirmRoutes = () => {
return (
<Routes>
<Route path="/:instance/:firm/*" element={
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
<Route path="/entities/*" element={ <EntityRoutes /> } />
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
<Route path="/templates/*" element={ <TemplateRoutes /> } />
<Route path="/drafts/*" element={ <DraftRoutes /> } />
<Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes>
</FirmContextProvider>
<ErrorBoundary fallback={<><Header /><Error404Page /></>} >
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
<Route path="/entities/*" element={ <EntityRoutes /> } />
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
<Route path="/templates/*" element={ <TemplateRoutes /> } />
<Route path="/drafts/*" element={ <DraftRoutes /> } />
<Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes>
</FirmContextProvider>
</ErrorBoundary>
} />
</Routes>
);
}
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 (
<>
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
<h1>{t("dashboard.title")}</h1>
<ul>
<li><Link to="entities">Entitées</Link></li>
<li><Link to="provisions">Templates de Clauses</Link></li>
<li><Link to="templates">Templates de Contrats</Link></li>
<li><Link to="drafts">Brouillons</Link></li>
<li><Link to="contracts">Contrats</Link></li>
<li><Link to="entities">{t("schemas.entity.resource_plural")}</Link></li>
<li><Link to="provisions">{t("schemas.provision_template.resource_plural")}</Link></li>
<li><Link to="templates">{t("schemas.contract_template.resource_plural")}</Link></li>
<li><Link to="drafts">{t("schemas.contract_draft.resource_plural")}</Link></li>
<li><Link to="contracts">{t("schemas.contract.resource_plural")}</Link></li>
</ul>
</>
);
}
@@ -62,18 +55,28 @@ type FirmInitFormPros = {
currentFirm: IFirm
}
const FirmInitForm = (props: FirmInitFormPros) => {
export 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)}
/>
)
}

View File

@@ -12,7 +12,7 @@ const DEFAULT_LOGIN_REDIRECT = "/hub"
const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to");
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param
const redirect = to_param === null ? getLoginRedirect() : to_param
if (providerName) {
let scope = {};
if (providerName === "google") {
@@ -61,18 +61,22 @@ const authProvider: AuthProvider = {
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
if (response.status == 204 || response.status == 401) {
forget_user();
return {
success: true,
redirectTo: "/",
};
return { success: true };
}
return { success: false };
},
check: async () => {
if (get_user() == null) {
const user = get_user();
if (user == null || isEmpty(user)) {
const user_data = await get_me();
if (user_data) {
store_user(user_data)
return { authenticated: true }
}
return {
authenticated: false,
redirectTo: "/login",
logout: true
}
}
@@ -84,11 +88,7 @@ const authProvider: AuthProvider = {
return user;
}
const response = await fetch(`${API_URL}/hub/users/me`);
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
const user_data = get_me()
store_user(user_data)
return user_data;
@@ -154,9 +154,8 @@ const authProvider: AuthProvider = {
if (error?.status === 401) {
forget_user();
return {
redirectTo: "/login",
logout: true,
error: { message: "Authentication required" },
logout: true,
} as OnErrorResponse;
}
else if (error?.status === 403) {
@@ -170,6 +169,15 @@ const authProvider: AuthProvider = {
},
};
async function get_me() {
const response = await fetch(`${API_URL}/hub/users/me`);
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
return user_data
}
function store_user(user: any) {
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
}
@@ -200,4 +208,11 @@ function findGetParameter(parameterName: string) {
return result;
}
function getLoginRedirect() {
if (location.pathname == "/login") {
return DEFAULT_LOGIN_REDIRECT
}
return location.pathname + location.search;
}
export default authProvider;

View File

@@ -2,6 +2,18 @@ import type { DataProvider, HttpError } from "@refinedev/core";
const API_URL = "/api/v1";
function handleErrors(response: { status: number, statusText: string }) {
let message = response.statusText
if (response.status == 405) {
message = "Resource is not ready";
}
const error: HttpError = {
message: message,
statusCode: response.status,
};
return Promise.reject(error);
}
const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
if (id === "") {
@@ -9,14 +21,7 @@ const dataProvider: DataProvider = {
}
const response = await fetch(`${API_URL}/${resource}/${id}`);
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
return handleErrors(response);
}
const data = await response.json();
@@ -35,17 +40,10 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
return handleErrors(response);
}
const data = await response.json();
const data = await response.json();
return { data };
},
getList: async ({ resource, pagination, filters, sorters, meta }) => {
@@ -72,14 +70,7 @@ const dataProvider: DataProvider = {
const response = await fetch(`${API_URL}/${resource}/?${params.toString()}`);
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
return handleErrors(response);
}
const data = await response.json();
@@ -105,18 +96,10 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
return handleErrors(response);
}
const data = await response.json();
return { data };
},
deleteOne: async ({ resource, id, variables, meta }) => {
@@ -125,21 +108,11 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
return handleErrors(response);
}
const data = await response.json();
return {
data
};
return { data };
},
getApiUrl: () => API_URL,
// Optional methods: