Compare commits

...

135 Commits

Author SHA1 Message Date
2ff15cdef4 Refactoring schema fetching in hooks 2025-05-14 19:08:15 +02:00
72b6f26ebc Minor refacto 2025-05-14 18:27:15 +02:00
b06ce4eefd Correcting typo in foreignkey 2025-05-14 18:26:52 +02:00
49317e905b Handling Variables in form 2025-05-14 18:26:24 +02:00
189c896e60 New columns for Template lists 2025-05-13 21:22:48 +02:00
4ae0b321b9 Correcting foreign key short comings 2025-05-13 21:22:25 +02:00
5a3b87e82c Correcting key position in looping component 2025-05-13 20:31:00 +02:00
0731ac3b6e Implementing enum references for columns and filters 2025-05-13 20:30:34 +02:00
77fa4cde35 Correction on core models and schemas 2025-05-13 16:24:20 +02:00
9aac1d3e34 Minor refactoring of schema provider 2025-05-13 16:22:58 +02:00
7aced9477c Localization provider for muix 2025-05-13 03:37:21 +02:00
5a0327c930 Dynamic Filters 2025-05-13 03:37:06 +02:00
18e4fcea28 Upgrading node packages 2025-05-11 17:04:52 +02:00
239ab2e241 Adding Crud filters form 2025-05-11 17:04:35 +02:00
9fcead8c95 Removing trailing console log 2025-05-11 14:45:16 +02:00
73871ae04e Correcting contract filters 2025-05-11 14:44:40 +02:00
c35c63b421 Adding timestamps filters to all Documents 2025-05-11 14:44:18 +02:00
9fd201c10a WIP trying to correct firm initialization 2025-05-07 17:39:37 +02:00
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
76143a9c2f Adding translation for ressource titles 2025-04-29 23:05:51 +02:00
1ba9a66c8e Updating translations and adding a translation tracker 2025-04-28 18:55:06 +02:00
14aea2a475 Moving auth pages back to the root 2025-04-28 01:24:54 +02:00
d38bb7d986 Implementing I18N 2025-04-27 19:47:03 +02:00
f71dccf166 WIP - starting to implement I18n 2025-04-27 17:31:27 +02:00
cc73fc4af2 Default exporting providers 2025-04-27 17:31:27 +02:00
76a5c0b454 Repairing Edit Form buttons 2025-04-27 17:26:12 +02:00
2b7a92097c Importing Skip jsonSchema 2025-04-27 15:54:19 +02:00
c9f8c69e42 Adding labels to drafts 2025-04-27 15:53:48 +02:00
bc41823dc3 Créating an official foreign key field 2025-04-27 01:21:05 +02:00
6c2047033b Prefilled drafts 2025-04-26 01:07:39 +02:00
6c3f6c8d03 Correcting data provider path 2025-04-26 01:07:08 +02:00
e01430f60e Adding the injection of a default value in the form 2025-04-23 00:06:23 +02:00
9d835d49d9 Correcting error in foreign key 2025-04-22 21:59:52 +02:00
081b3d08dd Separating Crud and Base form logic 2025-04-22 21:22:21 +02:00
614dc19095 Exporting foreign key json type 2025-04-22 21:20:38 +02:00
f0bf294d3d Updating foreign keys 2025-04-22 01:13:09 +02:00
272a1f61af Dynamic Schema names for crud 2025-04-22 00:31:39 +02:00
7b6ca62d9a Migrating foreign-key to his new fish tank 2025-04-21 20:24:43 +02:00
9e823d003e Forcing usage of id to getOne 2025-04-21 20:24:27 +02:00
309b55f25f Adding fixtures for provisions 2025-04-21 20:18:01 +02:00
ee9eb97262 Improving props drilling for resources and resources path 2025-04-21 15:44:24 +02:00
484246bd5d Updating dataprovider like filter 2025-04-21 15:39:27 +02:00
71b9c42265 Updating resource paths for foreign keys 2025-04-21 15:38:58 +02:00
bf02c4c10d Adding entity fixtures 2025-04-21 15:38:09 +02:00
3005c94010 Standarzing the TextWidget widget 2025-04-21 15:37:33 +02:00
6c480a4971 Adding the rich text widget 2025-04-21 01:30:16 +02:00
3fbb82642b Adding spinner to unloaded form (data or schema) 2025-04-21 01:29:45 +02:00
c90acc2765 Adding a theme to the ts app 2025-04-21 01:28:54 +02:00
8f950ed665 Small front corrections 2025-04-20 13:58:32 +02:00
2249791267 Implementing all cht classic routes 2025-04-19 01:32:26 +02:00
8766be57d0 Bringing back plural coherence in api routes 2025-04-19 01:29:31 +02:00
4b612fa7fe Listing entities 2025-04-18 23:57:51 +02:00
155a5edd7d WIP: Adding entity crud to front 2025-04-18 16:00:44 +02:00
fc5c63fe87 Correcting firm initialization return value 2025-04-18 11:18:49 +02:00
ef37d28e1b Raising error instead of juste instanciating it 2025-04-18 11:17:40 +02:00
e1e8ad79b4 Implentation of get current firm 2025-04-18 01:04:03 +02:00
654cf34c74 Updating hub firm resource path 2025-04-17 23:39:24 +02:00
13c5e078a8 Initializable Firm 2025-04-16 22:49:47 +02:00
5bdb754f1c Switching foreign jkey to PydanticObjectId 2025-04-16 22:38:06 +02:00
15b77ff09f Changing role to position in comployee model 2025-04-16 21:32:49 +02:00
b0c4128e01 Shaping CurrentFirmSchemaCreate arch 2025-04-16 19:43:00 +02:00
e5a2539ec6 Adding UiSchema capacities to CrudForm 2025-04-16 19:42:28 +02:00
8f9c1833b0 Switching to the registry paradigm 2025-04-15 21:13:06 +02:00
c7e946f963 Adding multi tenant check and Starting firm initialization 2025-04-12 18:12:56 +02:00
9ef599bbd5 Correcting bug on hub 2025-04-12 12:51:27 +02:00
ab3947b4a9 Even cleaner header and auth pages 2025-04-12 02:06:48 +02:00
b6f1e3eb8e Cleaner header and hub 2025-04-12 01:25:30 +02:00
b04ee4cb92 UPdating oauth callbacks 2025-04-11 22:15:56 +02:00
4e613554e6 Reformating route and renaming firm and correcting login redirections 2025-04-11 22:00:57 +02:00
d1718becde Refactoring Hub Firms routes 2025-04-11 21:58:04 +02:00
ef9ae99cb6 Adding chtlawfirm to the api 2025-04-11 00:04:47 +02:00
72a8e7fb91 Correcting bug in import 2025-04-10 18:15:00 +02:00
f1ad5d2965 Listing owned firms and working at firms 2025-04-10 13:31:57 +02:00
cda4f5654a Correcting auth-provider Error handling returns 2025-04-10 11:16:09 +02:00
f1fe81a146 Improving user management and auto-refreshing user firms 2025-04-10 01:31:37 +02:00
bc059de65b Implementing dataProvider and tuniong create route 2025-04-07 23:51:29 +02:00
661841ceef Importing RJSF lib with first application 2025-04-07 22:54:59 +02:00
f93a59b27e updating py gitignore 2025-04-07 19:12:56 +02:00
2a7a02a5c6 Upgrading vulnerables libs 2025-04-07 16:52:18 +02:00
684cdf0bb1 Layout Hub foundation 2025-04-07 16:48:39 +02:00
2c5724ccdd Reformating auth a bit 2025-04-07 15:47:18 +02:00
738a9bebf0 Full authentication in front 2025-04-07 02:33:27 +02:00
142 changed files with 16733 additions and 1887 deletions

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.10" /> <option name="sdkName" value="Python 3.10" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.13.2 Docker Compose (api)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.13.3 Docker Compose (api)" project-jdk-type="Python SDK" />
</project> </project>

View File

@@ -5,7 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/api/rpk-api" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/api/rpk-api" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/api/.venv" /> <excludeFolder url="file://$MODULE_DIR$/api/.venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Remote Python 3.13.2 Docker Compose (api)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Remote Python 3.13.3 Docker Compose (api)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

3
api/.gitignore vendored
View File

@@ -1 +1,2 @@
*/__pycache__/* __pycache__/
* .pyc

355
api/fixtures/entities.js Normal file
View File

@@ -0,0 +1,355 @@
db.Entity.insertMany([
{
"_id": ObjectId("640e297191d0c1b8d9caa30c"),
"address": "Barbareno Road - Chumash, SA",
"created_at": "2023-03-12T18:46:23.366Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Nathaniel",
"middlename": "",
"lastname": "Toshi",
"surnames": [],
"day_of_birth": "1992-08-11T00:00:00.000Z",
"place_of_birth": "Port Angeles, WA"
},
"label": "Nathaniel Toshi",
"updated_at": "2023-03-12T19:35:13.750Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("640e5b7391d0c1b8d9caa311"),
"address": "",
"created_at": "2023-03-12T18:46:23.366Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Nina",
"middlename": "",
"lastname": "Domingo",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Nina Cooper",
"updated_at": "2023-03-12T23:08:35.082Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("640e5bb991d0c1b8d9caa312"),
"address": "",
"created_at": "2023-03-12T18:46:23.366Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Derek",
"middlename": "",
"lastname": "Hillman",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Derek Hillman",
"updated_at": "2023-03-12T23:09:45.939Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105c7642f24b3cb93244cc"),
"address": "",
"created_at": "2023-03-13T18:03:45.999Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Andrew",
"middlename": "",
"lastname": "Moore",
"surnames": [],
"day_of_birth": "2002-03-12T00:00:00.000Z",
"place_of_birth": ""
},
"label": "Johnny Blackhand",
"updated_at": "2023-03-14T11:37:26.398Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105ce142f24b3cb93244ce"),
"address": "El Rancho Boulevard - Los Santos, SA",
"created_at": "2023-03-13T18:03:45.999Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "corporation",
"title": "East Custom",
"activity": "réparation de véhicule",
"employees": [
{
"position": "Dirigeant",
"entity_id": ObjectId("64105c7642f24b3cb93244cc")
}
]
},
"label": "Benny's",
"updated_at": "2023-03-14T11:39:13.143Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410721842f24b3cb93244da"),
"address": "",
"created_at": "2023-03-13T18:03:45.999Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Gary",
"middlename": "",
"lastname": "Terry",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Gary Terry",
"updated_at": "2023-03-14T13:09:44.325Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410724342f24b3cb93244dc"),
"address": "",
"created_at": "2023-03-13T18:03:45.999Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Wayne",
"middlename": "",
"lastname": "Terry",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Wayne Terry",
"updated_at": "2023-03-14T13:10:27.808Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e1b1c64aae93833c8f67"),
"address": "South Boulevard Del Perro - Los Santos, SA",
"created_at": "2023-03-14T14:07:16.735Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "corporation",
"title": "ROCKFORD",
"activity": "production et diffusion musicale",
"employees": []
},
"label": "RockFord",
"updated_at": "2023-03-14T21:05:53.499Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e21dc64aae93833c8f68"),
"address": "Murrieta Street - Los Santos, SA",
"created_at": "2023-03-14T14:07:16.735Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Lay",
"middlename": "",
"lastname": "Parks",
"surnames": [],
"day_of_birth": "2003-07-26T00:00:00.000Z",
"place_of_birth": "Philadelphie, PN"
},
"label": "Lay Parks",
"updated_at": "2023-03-14T21:07:41.072Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e2aec64aae93833c8f69"),
"address": "",
"created_at": "2023-03-14T14:07:16.735Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Franklin",
"middlename": "",
"lastname": "Woods",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Franklin Woods",
"updated_at": "2023-03-14T21:10:06.643Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64132ba1dfeec019e7312277"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Richard",
"middlename": "",
"lastname": "Malfaisant",
"surnames": ["Mauvaisefoi", "Ricard Malaisant", "Faisantmal"],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Richard \"Mauvaisefoi\" Malfaisant",
"updated_at": "2023-03-16T14:45:53.483Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6413493fdfeec019e731227e"),
"address": "Eclipse Tower",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "corporation",
"title": "LSHD",
"activity": "Hopital",
"employees": []
},
"label": "LSHD",
"updated_at": "2023-03-16T16:52:15.119Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64134981dfeec019e731227f"),
"address": "Avenue San Andreas",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "corporation",
"title": "BurgerShot",
"activity": "Restaurant",
"employees": []
},
"label": "BurgerShot",
"updated_at": "2023-03-16T16:53:21.827Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("641349bddfeec019e7312280"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Angelo",
"middlename": "",
"lastname": "Martinez",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Angelo Martinez",
"updated_at": "2023-03-16T16:54:21.552Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64134a22dfeec019e7312281"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Naytia",
"middlename": "",
"lastname": "Galloway",
"surnames": ["Nana"],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Naytia \"Nana\" Galloway",
"updated_at": "2023-03-16T16:56:02.133Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("641390fbdfeec019e7312286"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Ezrah",
"middlename": "",
"lastname": "KREIGHTON",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Ezrah KREIGHTON",
"updated_at": "2023-03-16T21:58:19.571Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6414920ddfeec019e7312288"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Kono",
"middlename": "",
"lastname": "Taylor",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Kono Taylor",
"updated_at": "2023-03-17T16:15:09.367Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6414b4e1dfeec019e731228b"),
"address": "",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Travis",
"middlename": "",
"lastname": "Ndiaye",
"surnames": [],
"day_of_birth": null,
"place_of_birth": ""
},
"label": "Travis Ndiaye",
"updated_at": "2023-03-17T18:43:45.434Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6414bcd2dfeec019e731228e"),
"address": "Grove Street - Los Santos, SA",
"created_at": "2023-03-16T14:41:43.613Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Junior",
"middlename": "",
"lastname": "Myles",
"surnames": ["Juicy"],
"day_of_birth": "1998-09-16T00:00:00.000Z",
"place_of_birth": "Los Santos, SA"
},
"label": "Junior \"Juicy\" Miles",
"updated_at": "2023-03-17T19:17:38.654Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64175041e0e97628a53dc362"),
"address": "",
"created_at": "2023-03-19T13:00:28.665Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"entity_data": {
"type": "individual",
"firstname": "Cranston",
"middlename": "",
"lastname": "Bennett",
"surnames": [],
"day_of_birth": "1999-02-14T00:00:00.000Z",
"place_of_birth": ""
},
"label": "Cranston Bennett",
"updated_at": "2023-03-19T18:11:13.712Z",
"updated_by": ObjectId("67f248427ad1b215ae859319")
}
])

View File

@@ -0,0 +1,220 @@
db.ProvisionTemplate.insertMany([
{
"_id": ObjectId("64105db142f24b3cb93244cf"),
"body": "<p>Le présent contrat est conclu pour une durée indéterminée. <br>Aucune période d'essai n'est prévue dans le présent contrat.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - durée du contrat (indéterminée) - \"Durée du contrat\"",
"name": "Contrat de travail - durée du contrat (indéterminée - sans perriode d'essai)",
"title": "Durée du contrat",
"updated_at": "2023-03-14T11:42:41.347Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105dee42f24b3cb93244d0"),
"body": "<p>Le lieu de travail habituel est situé à <strong>%ADRESSE_LIEU_DE_TRAVAIL%</strong>. Il se peut que dans le cadre dévènements, <strong>LEMPLOYÉ·E</strong> soit amené à travailler à lextérieur du lieu de travail.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - Lieu de travail - \"Lieu de travail\"",
"name": "Contrat de travail - Lieu de travail",
"title": "Lieu de travail",
"updated_at": "2023-03-14T11:43:42.786Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105e6942f24b3cb93244d1"),
"body": "<p id=\"docs-internal-guid-00a7ebe9-7fff-589c-16c6-32415839200c\" dir=\"ltr\">La rémunération nette horaire est déterminée en fonction des bénéfices imputables à <strong>LEMPLOYÉ·E</strong> au cours de la semaine.</p>\n<p dir=\"ltr\"><strong>LEMPLOYÉ·E</strong> obtiendra également une prime hebdomadaire en fonction du travail fourni.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - Rémunération et avantages (selon les bénéfices) - \"Rémunération et avantages\"",
"name": "Contrat de travail - Rémunération et avantages (selon les bénéfices)",
"title": "Rémunération et avantages",
"updated_at": "2023-03-14T11:45:45.465Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105eb942f24b3cb93244d2"),
"body": "<p>La prise de service est libre, mais cependant, <strong>LEMPLOYÉ·E</strong> devra prévenir <strong>LEMPLOYEUR</strong> au plus tôt 2 jours de toute absence prolongée, ou dindisponibilité pour raison personnelle ou impérieuse. Sous peine de sanction ou de de licenciement.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contat de travail - Durée du travail et service - \"Durée du travail et service\"",
"name": "Contrat de travail - Durée du travail et service",
"title": "Durée du travail et service",
"updated_at": "2023-03-14T11:47:05.528Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105f3642f24b3cb93244d3"),
"body": "<p id=\"docs-internal-guid-98edddc5-7fff-e483-a888-efc9298b4efd\" dir=\"ltr\">Pendant toute la durée du contrat, <strong>LEMPLOYÉ·E</strong> sengage à consacrer professionnellement toute son activité et tous ses soins à lentreprise, à observer le règlement intérieur, toutes les consignes et instructions particulières de travail qui lui seront données.</p>\n<p dir=\"ltr\"><strong>LEMPLOYÉ·E</strong> conserve la propriété des moyens techniques, outils, méthodes ou savoir-faire (à l'exclusion des moyens techniques, outils, méthodes, procédés et savoir-faire qui lui ont été transmis par <strong>LEMPLOYEUR</strong>)</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contat de travail - Obligations professionnelles - \"Obligations professionnelles\"",
"name": "Contrat de travail - Obligations professionnelles",
"title": "Obligations professionnelles",
"updated_at": "2023-03-14T11:49:10.015Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("64105ff042f24b3cb93244d4"),
"body": "<p><strong>LEMPLOYÉ·E</strong> aura pour mission principale de %MISSION_PRINCIPALE%.<br>La liste des missions nest pas exhaustive. <strong>LEMPLOYEUR</strong> se réserve le droit de faire exécuter à <strong>LEMPLOYÉ·E</strong> toute tâche nécessaire au bon fonctionnement de lentreprise.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - Missions - \"Missions\"",
"name": "Contrat de travail - Missions",
"title": "Missions",
"updated_at": "2023-03-14T11:52:16.531Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410605342f24b3cb93244d5"),
"body": "<p>Afin dexécuter les tâches qui lui sont affectées, <strong>LEMPLOYÉ·E</strong> aura à sa disposition plusieurs véhicules qui restent à la propriété de l´entreprise.</p>\n<p>Lentretien des véhicules est à la charge de l´entreprise .</p>\n<p>En cas de panne ou daccident avec un des véhicules, les frais engagés seront pris en charge par l´entreprise</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - - \"Clauses relatives au matériel\"",
"name": "Contrat de travail - Clauses relatives au matériel",
"title": "Clauses relatives au matériel",
"updated_at": "2023-03-14T11:53:55.804Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("641060a042f24b3cb93244d6"),
"body": "<p>Chacune des parties pourra mettre fin au présent contrat, sous réserve de respecter les règles prévues à cet effet par la loi et de prévenir lautre partie de sa décision par lettre recommandée avec accusé de réception. </p>\n<p>En cas dajout au casier judiciaire après lembauche de <strong>LEMPLOYÉ·E</strong>, celui-ci pourra être, à la discretion de <strong>LEMPLOYEUR</strong> licencié.<br>La période de préavis pour les deux parties est de 48 heures.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - Rupture de contrat - \"Rupture de contrat\"",
"name": "Contrat de travail - Rupture de contrat",
"title": "Rupture de contrat",
"updated_at": "2023-03-14T11:55:12.879Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("641060f242f24b3cb93244d7"),
"body": "<p><strong>LEMPLOYÉ·E</strong> est tenu·e à une obligation de bonne foi dans lexercice de ses fonctions.</p>\n<p>Sous peine dencourir des sanctions disciplinaires voire pénales, <strong>LEMPLOYÉ·E</strong> sengage à ne pas divulguer dinformations confidentielles sur l'entreprise ainsi qu'à garder le secret le plus absolu pendant la durée de son service et après l'avoir quitté, y compris après la rupture éventuelle de l'engagement contractuel présent, sur tout ce qui a été appris ou communiqué, par n'importe quel moyen -verbal ou écrit, que ce soit à l'intérieur ou à l'extérieur de l'entreprise.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - Clause de loyauté, de bonne foi et de confidentialité - \"Clause de loyauté, de bonne foi et de confidentialité \"",
"name": "Contrat de travail - Clause de loyauté, de bonne foi et de confidentialité ",
"title": "Clause de loyauté, de bonne foi et de confidentialité ",
"updated_at": "2023-03-14T11:56:34.871Z"
},
{
"_id": ObjectId("6410715742f24b3cb93244d9"),
"body": "<p>Le présent contrat est conclu pour une durée de <strong>%DUREE_DU_CONTRAT%</strong>.</p>\n<p>A l'expiration de cette perriode, un nouveau contrat devra être établi entre les deux parties.</p>",
"created_at": "2023-03-13T18:03:45.999Z",
"label": "Contrat de travail - durée du contrat (déterminée) - \"Durée du contrat\"",
"name": "Contrat de travail - durée du contrat (déterminée)",
"title": "Durée du contrat",
"updated_at": "2023-03-14T13:06:31.373Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e4c7c64aae93833c8f6a"),
"body": "<p>Les <strong>parties</strong> conviennent des définitions suivantes :</p>\n<p><strong>Album </strong>: ensemble constitué dune succession dun minimum de 10 (dix) phonogrammes pour une durée totale d'enregistrement dau moins 40 (quarante) minutes.</p>\n<p><strong>Album Inédit Studio</strong> : Album dœuvres musicales nouvelles et inédites (paroles et musiques), chantées - enregistrées en studio et interprétées par lARTISTE en toute langue préalablement agréée par écrit par le PRODUCTEUR.</p>\n<p><strong>Date de sortie commerciale</strong> : Date de mise à disposition du public des enregistrements objet des présentes, telle que formalisée par la feuille d'information publiée par le PRODUCTEUR à destination de sa clientèle</p>\n<p><strong>Durée dexclusivité de fixation</strong> : Durée pendant laquelle lARTISTE consent au PRODUCTEUR lexclusivité de fixation de ses prestations, telle quelle est définie à larticle 4 du présent contrat.</p>\n<p><strong>Phonogramme </strong>: Toute fixation exclusivement sonore de sons provenant de lexécution<br>instrumentale et/ou linterprétation vocale de toute œuvre musicale avec ou sans paroles, quels quen soient le procédé denregistrement et la destination.</p>\n<p><strong>Enregistrement</strong>: Fixation de sons ou dimages sonorisées ou non dune prestation de lARTISTE, quels quen soient le procédé denregistrement et la destination.</p>\n<p><strong>Bande mère</strong> : Support original (incluant les multipistes) contenant les versions définitives mixées et masterisées dun phonogramme ou dun vidéogramme ou dun ensemble constitué dune succession de phonogrammes et/ou de vidéogrammes permettant les opérations de production.</p>\n<p><strong>Maquette </strong>: Version pré-produite dun titre enregistré par lARTISTE en studio, dune qualité technique et artistique suffisamment élaborée pour permettre au PRODUCTEUR davoir une idée précise de ce que sera la version définitive dudit titre une fois produit.Mise à disposition du public : toutes modalités de distribution par la vente, léchange ou le louage y compris toute forme de distribution par réseaux numériques de données, des phonogrammes et vidéogrammes, mettant en œuvre le droit dautorisation préalable du PRODUCTEUR en qualité de producteur au sens de la propriété intellectuelle.</p>\n<p><strong>Réalisateur artistique</strong> : Personne responsable de l'organisation des sessions d'enregistrement, du contrôle de la qualité des enregistrements et qui assure leur bonne fin et toutes autres tâches similaires ou annexes.</p>\n<p><strong>Single </strong>: Programme constitué dune succession dun maximum de 3 (trois) phonogrammes dune durée minimale chacun de 3 minutes.</p>\n<p><strong>Support Phonographique</strong> : Tout support matériel permettant la fixation et/ou la reproduction du son, quel quen soit le procédé denregistrement connu ou inconnu à ce jour, quelle que soit la nature du support mécanique, magnétique, acoustique, numérique, optique ou autres, et quelle que soit la destination.</p>\n<p><strong>Territoire </strong>: Le monde entier.</p>\n<p><strong>Vidéogramme </strong>: Toute fixation de séquences dimages sonorisées ou non quels quen soient le procédé denregistrement et la destination.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Définitions - \"Définitions\"",
"name": "Contrat d'exploitation - Définitions",
"title": "Définitions",
"updated_at": "2023-03-14T21:19:03.050Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e51fc64aae93833c8f6b"),
"body": "<p id=\"docs-internal-guid-3986f82b-7fff-5815-55ab-34db6ee4d510\" dir=\"ltr\" style=\"text-align: justify;\"><strong>2-1</strong> Le <strong>PRODUCTEUR </strong>engage <strong>l'ARTISTE </strong>en vue de la fixation et de lexploitation par quelque procédé que ce soit de ses prestations artistiques à des fins commerciales et promotionnelles.</p>\n<p dir=\"ltr\" style=\"text-align: justify;\">Le présent contrat est conclu en conformité avec les règles relatives aux contrats à durée</p>\n<p dir=\"ltr\" style=\"text-align: justify;\">indéterminée dits \"d'usage\" dans le secteur de l'édition phonographique.Il s'applique à un emploi à caractère artistique, par nature indéterminé.</p>\n<p dir=\"ltr\" style=\"text-align: justify;\"><strong>2-2</strong> <strong>L'ARTISTE</strong>, qui se déclare libre de tout engagement similaire, accorde au <strong>PRODUCTEUR </strong>l'exclusivité de la fixation de ses prestations pour le monde entier en toutes langues, en vue de leur communication par tous moyens connus ou à venir par tous procédés actuels ou à venir, notamment sur des supports matériels ou non quelconques les reproduisant pour une publication commerciale ou non commerciale.</p>\n<p><strong>L'ARTISTE </strong>certifie ne pas être lié à ce jour par un quelconque contrat interdisant ou pouvant gêner la conclusion et/ou lexécution des présentes et garantit le <strong>PRODUCTEUR </strong>contre tout recours et/ou réclamation à cet égard.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Objet - \"Objet\"",
"name": "Contrat d'exploitation - Objet",
"title": "Objet",
"updated_at": "2023-03-14T21:20:31.260Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e540c64aae93833c8f6c"),
"body": "<p id=\"docs-internal-guid-f2311b4a-7fff-0b9a-c6fb-7b4cd4f3f8f6\" dir=\"ltr\" style=\"text-align: justify;\"><strong>3-1</strong> Compte tenu des investissements consentis par le <strong>PRODUCTEUR </strong>pour la production et la promotion des enregistrements de <strong>l'ARTISTE </strong>produits dans le cadre du présent contrat, le<strong> LABEL </strong>sinterdit, à lexpiration de la Durée dexclusivité de fixation, d'enregistrer, produire, distribuer ou vendre, soit pour son propre compte, soit pour le compte dun tiers, d'autres interprétations des œuvres enregistrées en exécution du présent contrat pendant une durée maximum de 5 (cinq) années à compter de l'expiration de la durée dexclusivité de fixation.</p>\n<p>Il garantit par ailleurs qu'il est libre de les enregistrer pour le compte du <strong>PRODUCTEUR</strong>.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Exclusivité - \"Exclusivité\"",
"name": "Contrat d'édition- Exclusivité",
"title": "Exclusivité",
"updated_at": "2023-03-14T21:21:04.467Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e56ac64aae93833c8f6d"),
"body": "<p>4.1 L'ARTISTE se déclare pleinement habilité à conclure le présent contrat.</p>\n<p>4.2 L'ARTISTE certifie n'être pas lié à ce jour par un quelconque contrat interdisant ou pouvant gêner la conclusion et/ou lexécution des présentes. LARTISTE sengage à ne pas autoriser pendant la Durée dexclusivité de fixation, la commercialisation denregistrements fixés antérieurement à la date des présentes pour son propre<br>compte ou pour le compte dun tiers et non encore publiés.</p>\n<p>4.3 LARTISTE garantit au PRODUCTEUR la jouissance et lexploitation paisible de son nom patronymique et/ou de son pseudonyme.</p>\n<p>4.4 L'ARTISTE reconnaît que la déclaration ci-dessus engage son entière responsabilité, et qu'il sera responsable de toutes les pertes et du préjudice subi par le PRODUCTEUR du fait dune fausse déclaration.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Garanties - \"Garanties\"",
"name": "Contrat d'exploitation - Garanties",
"title": "Garanties",
"updated_at": "2023-03-14T21:21:46.107Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e5b5c64aae93833c8f6f"),
"body": "<p>5.1 L'ARTISTE cède irrévocablement, pour le Monde, au PRODUCTEUR qui laccepte, le droit exclusif, sans restriction ni réserve, de fixer ou autoriser la fixation, reproduire ou autoriser la reproduction, communiquer ou autoriser la communication au public, de toutes les prestations et interprétations fixées par l'ARTISTE pendant la durée du présent contrat, ainsi que tous les droits patrimoniaux présents et futurs s'y rattachant. </p>\n<p>Ces droits comprennent notamment :</p>\n<ul>\n<li>Le droit de fixer la prestation de lARTISTE</li>\n<li> Le droit exclusif de reproduire, et faire reproduire, fabriquer et faire fabriquer, publier et faire publier, mettre à la disposition du public par la vente, la location ou le prêt, sous toutes formes marques et étiquettes au choix du PRODUCTEUR et au prix quelle fixera, les prestations et interprétations de lARTISTE et plus généralement toute fixation de ces dernières, associées ou non à limage, sous toutes formes connues ou à découvrir, à toutes fins, en entier ou par extraits, sur tous supports et configurations connus ou à découvrir.</li>\n<li> Le droit exclusif de communication au public et de mise à disposition du public en entier ou par extraits, par tous moyens connus ou à découvrir, notamment par lintermédiaire de réseaux de transports de données avec ou sans fil (tels que le réseau internet, les réseaux de téléphonie mobile...), par projection, diffusion radioélectrique, satellite, télématique, réseaux informatiques et interactifs on-line ou off-line, câblodistribution, réseau télévisuel, des prestations et interprétations de lARTISTE (y compris non fixées), associées ou non à limage, à toutes fins, sous toutes formes et configurations connues ou à découvrir</li>\n</ul>\n<p>5.2 L'ARTISTE reconnaît, sans restrictions ni réserves, que le PRODUCTEUR est seule propriétaire des biens meubles que constituent les bandes reproduisant les interprétations de lARTISTE enregistrées en application des présentes et lARTISTE reconnaît que le PRODUCTEUR est réputée seul producteur des phonogrammes, au sens du Code de la Propriété Intellectuelle.</p>\n<p>5.3 Le PRODUCTEUR exercera en conséquence les droits reconnus à lARTISTE par la loi comme ses droits propres.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Cession des droits - \"Cession des droits\"",
"name": "Contrat d'exploitation - Cession des droits",
"title": "Cession des droits",
"updated_at": "2023-03-14T21:23:01.572Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e5e2c64aae93833c8f70"),
"body": "<p>6.1 L'ARTISTE s'engage à soumettre au PRODUCTEUR avant toute entrée en studio les maquettes des œuvres qu'il souhaiterait enregistrer. Le PRODUCTEUR pourra également proposer des projets dœuvres à interpréter.<br>Le choix des œuvres à enregistrer est ensuite effectué d'un commun accord entre l'ARTISTE et le PRODUCTEUR.</p>\n<p>Les séances en studio ne pourront débuter quaprès validation définitive par écrit par le PRODUCTEUR du projet denregistrement. Les dates des séances d'enregistrement seront déterminées par le PRODUCTEUR et l'ARTISTE d'un commun accord.</p>\n<p>6.2 L'ARTISTE s'engage à se présenter aux séances d'enregistrement prêt à enregistrer les titres<br>sélectionnés.</p>\n<p>6.3 L'ARTISTE s'engage à venir aux séances d'enregistrement prêt à réaliser l'enregistrement définitif, le PRODUCTEUR restant seul juge du résultat définitif.</p>\n<p>6.4 L'ARTISTE respectera le règlement intérieur des studios d'enregistrement.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Séances denregistrement en studio - \"Séances denregistrement en studio\"",
"name": "Contrat d'exploitation - Séances denregistrement en studio",
"title": "Séances denregistrement en studio",
"updated_at": "2023-03-14T21:23:46.930Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6410e63ac64aae93833c8f71"),
"body": "<p>7.1 Les Réalisateurs Artistiques, mixeurs et éventuels remixeurs et Beatmaker sont choisis par le PRODUCTEUR en concertation avec l'ARTISTE. La rémunération des Réalisateurs Artistiques, mixeurs ou remixeurs extérieurs est à la charge du PRODUCTEUR comme suit : <br>Lartiste ayant signé ce contrat recevra une rémunération au nombre découtes des auditeurs sur les différentes plateformes de streaming : </p>\n<ul>\n<li>$250 par écoute pour le BEAT-MAKER</li>\n<li>$500 par écoute pour lARTISTE</li>\n</ul>\n<p>7.2 Le PRODUCTEUR se réserve le droit de rémunérer à titre exceptionnel une prime lors des concert/événements.</p>",
"created_at": "2023-03-14T14:07:16.735Z",
"label": "Contrat d'exploitation - Rémunération des réalisateurs artistiques, (re-)mixeurs, et beatmakers - \"Rémunération des réalisateurs artistiques, (re-)mixeurs, et beatmakers\"",
"name": "Contrat d'exploitation - Rémunération des réalisateurs artistiques, (re-)mixeurs, et beatmakers",
"title": "Rémunération des réalisateurs artistiques, (re-)mixeurs, et beatmakers",
"updated_at": "2023-03-14T21:25:14.798Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6413477adfeec019e7312278"),
"body": "<p dir=\"ltr\">Le CLIENT confie au RESTAURATEUR, qui accepte, la mission de fournir les prestations ci-après définies en vue de la restauration de son personnel, dans son établissement situé :</p>\n<p dir=\"ltr\"><strong>Eclipse Tower<br><br></strong>Le RESTAURATEUR fournira ses prestations en toute indépendance pour les quantités suivantes : 200 Menu Simple au prix de 45 $ le menu pour un total de 9000$</p>\n<p dir=\"ltr\">Menu Simple : </p>\n<div dir=\"ltr\" align=\"left\">\n<div dir=\"ltr\" align=\"left\">\n<table><colgroup></colgroup>\n<tbody>\n<tr>\n<td>\n<p dir=\"ltr\">Burger viande ou poisson</p>\n</td>\n</tr>\n<tr>\n<td>\n<p dir=\"ltr\">Soda</p>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n<strong id=\"docs-internal-guid-3e52c7d0-7fff-f92b-674f-d405d3884c38\"></strong></div>\n<p> </p>",
"created_at": "2023-03-16T14:41:43.613Z",
"label": "Contrat de Prestation de Service - \" Objet du contrat \"",
"name": "Contrat de Prestation de Service - Objet du contrat",
"title": "Objet du contrat",
"updated_at": "2023-03-16T16:44:42.446Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("641347f4dfeec019e7312279"),
"body": "<p dir=\"ltr\">Le présent contrat prend effet le %DATE_EFFET_CONTRAT%.</p>\n<p dir=\"ltr\">Il est conclu pour une durée dune semaine renouvelable tacitement, chacune des parties ayant la possibilité d'en faire cesser l'effet à tout moment, à la condition expresse de prévenir l'autre partie par décision écrite et datée.</p>",
"created_at": "2023-03-16T14:41:43.613Z",
"label": "Contrat de Prestation de Service - Durée du contrat - \"Durée du contrat\"",
"name": "Contrat de Prestation de Service - Durée du contrat",
"title": "Durée du contrat",
"updated_at": "2023-03-16T16:46:44.703Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
},
{
"_id": ObjectId("6413482edfeec019e731227a"),
"body": "<p dir=\"ltr\">La rupture sera effective le premier jour de la semaine suivant sa notification écrite.</p>\n<ul>\n<li dir=\"ltr\" role=\"presentation\">En cas dannulation à linitiative du CLIENT, celui-ci sengage à verser au RESTAURATEUR léquivalent du prix dune semaine de prestation sans contrepartie.</li>\n<li>En cas dannulation à linitiative du RESTAURATEUR, celui-ci sengage à livrer gracieusement la commande de la semaine suivant lannonce de la rupture.</li>\n</ul>",
"created_at": "2023-03-16T14:41:43.613Z",
"label": "Contrat de Prestation de Service - Rupture du contrat - \"Rupture du contrat\"",
"name": "Contrat de Prestation de Service - Rupture du contrat",
"title": "Rupture du contrat",
"updated_at": "2023-03-16T16:47:42.232Z",
"created_by": ObjectId("67f248427ad1b215ae859319"),
"updated_by": ObjectId("67f248427ad1b215ae859319")
}
])

View File

@@ -1,4 +1,8 @@
fastapi fastapi
fastapi-filter
fastapi-pagination
fastapi-users[beanie,oauth] fastapi-users[beanie,oauth]
uvicorn
httpx-oauth httpx-oauth
jinja2
uvicorn
weasyprint

30
api/rpk-api/cli.py Normal file
View File

@@ -0,0 +1,30 @@
import asyncio
import contextlib
from fastapi_users.exceptions import UserAlreadyExists
from fastapi_users.schemas import BaseUserCreate
from hub import init_db as hub_init_db
from hub.auth import get_user_manager
from hub.user import get_user_db
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, is_superuser: bool = False):
try:
await hub_init_db()
async with get_user_db_context() as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await user_manager.create(
BaseUserCreate(
email=email, password=password, is_superuser=is_superuser
)
)
print(f"User created {user}")
except UserAlreadyExists:
print(f"User {email} already exists")
if __name__ == "__main__":
asyncio.run(create_user("test@test.te", "test"))

View File

@@ -0,0 +1,14 @@
from fastapi import APIRouter
from firm.entity import entity_router
from firm.template import template_router
from firm.contract import contract_router
from firm.current_firm.routes import current_firm_router
firm_router = APIRouter(prefix="/{instance}/{firm}")
firm_router.include_router(current_firm_router, tags=["Current Firm"])
firm_router.include_router(entity_router, prefix="/entities", tags=["Entity"], )
firm_router.include_router(template_router, prefix="/templates", tags=["Template"], )
firm_router.include_router(contract_router, prefix="/contracts", )

View File

@@ -0,0 +1,13 @@
from fastapi import APIRouter
from firm.contract.routes_contract import contract_router as contract_subrouter
from firm.contract.routes_signature import signature_router
from firm.contract.routes_draft import draft_router
from firm.contract.print import print_router, preview_router
contract_router = APIRouter()
contract_router.include_router(draft_router, prefix="/drafts", tags=["Contract Draft"], )
contract_router.include_router(contract_subrouter, tags=["Contract"], )
contract_router.include_router(preview_router, prefix="/preview", )
contract_router.include_router(print_router, prefix="/print", )
contract_router.include_router(signature_router, prefix="/signature", tags=["Signature"], )

View File

@@ -0,0 +1,224 @@
import datetime
from typing import List, Literal, Optional
from enum import Enum
from uuid import UUID
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
from pydantic.json_schema import SkipJsonSchema
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey, \
CrudDocumentConfig
from firm.core.filter import Filter, FilterSchema
from firm.entity.models import Entity
class ContractStatus(str, Enum):
published = 'published'
signed = 'signed'
printed = 'printed'
executed = 'executed'
canceled = 'canceled'
class ContractDraftStatus(str, Enum):
in_progress = 'in_progress'
ready = 'ready'
published = 'published'
class DraftParty(BaseModel):
model_config = ConfigDict(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):
entity: Entity
part: str
representative: Optional[Entity] = None
signature_uuid: str
signature_affixed: bool = False
signature_png: Optional[str] = None
class ContractProvisionType(Enum):
genuine = 'genuine'
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 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=None,
title="Template de clause"
)
class DraftProvision(BaseModel):
model_config = ConfigDict(title='Clause')
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Provision(BaseModel):
title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline(title="Corps")
class ContractDraftUpdateStatus(BaseModel):
status: str = Field()
todo: List[str] = Field(default=[])
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(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")
async def check_is_ready(self, db):
if self.status == ContractDraftStatus.published:
return
self.todo = []
if len(self.parties) < 2:
self.todo.append('Contract must have at least two parties')
if len(self.provisions) < 1:
self.todo.append('Contract must have at least one provision')
for p in self.parties:
if not p.entity_id:
self.todo.append('All parties must have an associated entity`')
for p in self.provisions:
if p.provision.type == "genuine" and not (p.provision.title and p.provision.body):
self.todo.append('Empty genuine provision')
elif p.provision.type == "template" and not p.provision.provision_template_id:
self.todo.append('Empty template provision')
for v in self.variables:
if not (v.key and v.value):
self.todo.append(f'Empty variable: {v.key}')
if self.todo:
self.status = ContractDraftStatus.in_progress
else:
self.status = ContractDraftStatus.ready
await self.update(db, self, ContractDraftUpdateStatus(status=self.status, todo=self.todo))
async def update_status(self, db, status):
update = ContractDraftUpdateStatus(status=status)
await self.update(db, self, update)
def compute_label(self) -> str:
return f"{self.name} - {self.title}"
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},
title='Clauses'
)
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date")
def compute_label(self) -> str:
contract_label = self.title
for p in self.parties:
contract_label = f"{contract_label} - {p.entity.label}"
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
return contract_label
@classmethod
async def find_by_signature_id(cls, db, signature_id: UUID):
request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}
value = await cls._get_collection(db).find_one(request)
return cls.model_validate(value) if value else None
def get_signature(self, signature_id: str):
for p in self.parties:
if p.signature_uuid == str(signature_id):
return p
def get_signature_index(self, signature_id: str):
for i, p in enumerate(self.parties):
if p.signature_uuid == str(signature_id):
return i
def is_signed(self):
for p in self.parties:
if not p.signature_affixed:
return False
return True
async def affix_signature(self, db, signature_index):
update_query = {"$set": {
f'parties.{signature_index}.signature_affixed': True
}}
self.parties[signature_index].signature_affixed = True
if self.is_signed():
update_query["$set"]['status'] = 'signed'
await self._get_collection(db).update_one({"_id": self.id}, update_query)
return await self.get(db, self.id)
def replace_variables_in_value(variables, value: str):
for v in variables:
if v.value:
value = value.replace('%{}%'.format(v.key), v.value)
return value
class ContractDraftFilters(FilterSchema):
status__in: Optional[list[str]] = None
class Constants(Filter.Constants):
model = ContractDraft
search_model_fields = ["label"]
class ContractFilters(FilterSchema):
status__in: Optional[list[str]] = None
class Constants(Filter.Constants):
model = Contract
search_model_fields = ["label"]

View File

@@ -0,0 +1,156 @@
import datetime
import os
import base64
from uuid import UUID
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration
from pathlib import Path
from firm.core.depends import get_tenant_registry
from firm.entity.models import Entity
from firm.template.models import ProvisionTemplate
from firm.contract.models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
async def build_model(model):
parties = []
for p in model.parties:
party = {
"entity": await Entity.get(p.entity_id),
"part": p.part
}
if p.representative_id:
party['representative'] = await Entity.get(p.representative_id)
parties.append(party)
model.parties = parties
provisions = []
for p in model.provisions:
if p.provision.type == "template":
provision = await ProvisionTemplate.get(p.provision.provision_template_id)
else:
provision = p.provision
provision.title = replace_variables_in_value(model.variables, provision.title)
provision.body = replace_variables_in_value(model.variables, provision.body)
provisions.append(provision)
model.provisions = provisions
model = model.dict()
model['location'] = "Los Santos, SA"
model['date'] = datetime.date(1970, 1, 1)
model['lawyer'] = {'entity_data': {
"firstname": "prénom avocat",
"lastname": "nom avocat",
}}
return model
BASE_PATH = Path(__file__).resolve().parent
templates = Jinja2Templates(directory=str(BASE_PATH / "templates"))
async def render_print(root_url, contract):
template = templates.get_template("print.html")
return template.render({
"contract": contract,
"root_url": root_url
})
async def render_css(root_url, contract):
template = templates.get_template("styles.css")
return template.render({
"contract": contract,
"root_url": root_url
})
def retrieve_signature_png(filepath):
with open(filepath, "rb") as f:
b_content = f.read()
base64_utf8_str = base64.b64encode(b_content).decode('utf-8')
ext = filepath.split('.')[-1]
return f'data:image/{ext};base64,{base64_utf8_str}'
preview_router = APIRouter()
@preview_router.get("/draft/{draft_id}", response_class=HTMLResponse, tags=["Contract Draft"])
async def preview_draft(draft_id: str, reg=Depends(get_tenant_registry)) -> str:
draft = await build_model(await ContractDraft.get(reg.db, draft_id))
return await render_print('', draft)
@preview_router.get("/signature/{signature_id}", response_class=HTMLResponse, tags=["Signature"])
async def preview_contract_by_signature(signature_id: UUID, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.find_by_signature_id(reg.db, signature_id)
for p in contract.parties:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print('', contract)
@preview_router.get("/{contract_id}", response_class=HTMLResponse, tags=["Contract"])
async def preview_contract(contract_id: str, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.get(reg.db, contract_id)
for p in contract.parties:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print('', contract)
print_router = APIRouter()
@print_router.get("/pdf/{contract_id}", response_class=FileResponse, tags=["Contract"])
async def create_pdf(contract_id: str, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.get(reg.db, contract_id)
contract_path = "media/contracts/{}.pdf".format(contract_id)
if not os.path.isfile(contract_path):
if contract.status != ContractStatus.signed:
raise HTTPException(status_code=400, detail="Contract is not in a printable state")
for p in contract.parties:
signature_path = f'media/signatures/{p.signature_uuid}.png'
p.signature_png = retrieve_signature_png(signature_path)
# os.remove(signature_path)
font_config = FontConfiguration()
html = HTML(string=await render_print('http://nginx', contract))
css = CSS(string=await render_css('http://nginx', contract), font_config=font_config)
html.write_pdf(contract_path, stylesheets=[css], font_config=font_config)
await contract.update_status(reg.db, 'printed')
return FileResponse(
contract_path,
media_type="application/pdf",
filename=contract.label)
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse, tags=["Signature"])
async def get_signature_opengraph(signature_id: str, request: Request, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.find_by_signature_id(reg.db, signature_id)
signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html")
signatory = signature.representative.label if signature.representative else signature.entity.label
return template.render({
"signatory": signatory,
"title": contract.label,
"origin_url": f"{request.url.scheme}://{request.url.hostname}"
})

View File

@@ -0,0 +1,10 @@
<html>
<head>
<meta property="og:title" content="{{ title }}" />
<meta property="og:description" content="Cette page est à la destination exclusive de {{ signatory }}
Si vous n'êtes pas {{ signatory }}, veuillez fermer cette page immédiatement et surpprimer tous les liens en votre possession menant vers celle-ci.
En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour usurpation d'identité et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.
Le cabinet Cooper, Hillman & Toshi LLC" />
<meta property="og:image" content="{{ origin_url }}/assets/logo.png" />
</head>
</html>

View File

@@ -0,0 +1,76 @@
<html>
<head>
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<div class="frontpage">
<div id="front-page-header">
<table><tr>
<td><img id="top-logo" src="{{ root_url }}/assets/logotransparent.png" alt="Cooper, Hillman & Toshi logo"></td>
<td id="office-info">Cooper, Hillman & Toshi LLP<br />6834 Innocence Boulevard<br />LOS SANTOS - SA<br /><a href="#">consulting@cht.law.com</a></td>
</tr></table>
<h1>{{ contract.title|upper }}</h1>
</div>
<div class="intro">
<h2>Introduction</h2>
<p>Le {{ contract.date.strftime('%d/%m/%Y') }} &agrave; {{ contract.location}}</p>
<p>Entre les soussign&eacute;s :</p>
{% for party in contract.parties %}
<div class="party">
{% if not loop.first %}
<p>ET</p>
{% endif %}
<p>
{% if party.entity.entity_data.type == "corporation" %}
{{ party.entity.entity_data.title }} soci&eacute;t&eacute; de {{ party.entity.entity_data.activity }} enregistr&eacute;e aupr&egrave;s du gouvernement de San Andreas et domicili&eacute;e au {{ party.entity.address }}{% if party.representative %}, repr&eacute;sent&eacute;e par {{ party.representative.entity_data.firstname }} {{ party.representative.entity_data.middlenames }} {{ party.representative.entity_data.lastname }}{% endif %}
{% elif party.entity.entity_data.type == "individual" %}
{{ party.entity.entity_data.firstname }} {{ party.entity.entity_data.middlenames }} {{ party.entity.entity_data.lastname }}
{% if party.entity.entity_data.day_of_birth %} n&eacute; le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if party.entity.entity_data.place_of_birth %} &agrave; {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
{% if party.entity.address %} r&eacute;sidant &agrave; {{ party.entity.address }}, {% endif %}
{% elif party.entity.entity_data.type == "institution" %}
{% endif %}
</p>
<p>Ci-apr&egrave;s d&eacute;nomm&eacute; <strong>{{ party.part|safe }}</strong></p>
{% if loop.first %}
<p class="part">d&apos;une part</p>
{% endif %}
</div>
{% endfor %}
<p class="part">d&apos;autre part</p>
<p>Sous la supervision l&eacute;gale de Ma&icirc;tre <strong>{{ contract.lawyer.entity_data.firstname }} {{ contract.lawyer.entity_data.lastname }}</strong></p>
<p>Il a &eacute;t&eacute; convenu l&apos;ex&eacute;cution des prestations ci-dessous, conform&eacute;ment aux conditions g&eacute;n&eacute;rales et particuli&egrave;res ci-apr&egrave;s:</p>
</div>
</div>
<div class="content">
<h2>Conditions g&eacute;n&eacute;rales & particuli&egrave;res</h2>
{% for provision in contract.provisions %}
<div class="provision">
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
<p>{{ provision.body|safe }}</p>
</div>
{% endfor %}
<div class="footer">
<hr/>
<p>À {{ contract.location }} le {{ contract.date.strftime('%d/%m/%Y') }}</p>
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p>
<table class="signatures">
<tr>
{% for party in contract.parties %}
<td>
{{ party.part|safe }}:<br/>
{% if party.signature_png %}
<img src="{{ party.signature_png }}" />
{% endif %}
</td>
{% endfor %}
</tr>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,147 @@
@font-face {
font-family: 'Century Schoolbook';
src: url('{{ root_url }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf');
}
@font-face {
font-family: "Century Schoolbook";
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBold.ttf");
font-weight: bold;
}
@font-face {
font-family: "Century Schoolbook";
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf");
font-style: italic;
}
@font-face {
font-family: "Century Schoolbook";
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf");
font-weight: bold;
font-style: italic;
}
@page{
size: a4 portrait;
margin: 2cm 2cm 2cm 2cm;
counter-increment: page;
@bottom-center {
content: "© Cooper, Hillman & Toshi LLC - {{ contract.name }} - Page " counter(page) "/" counter(pages);
font-size: 0.8em;
}
background: url('{{ root_url }}/assets/watermark.png') no-repeat;
background-size:contain;
}
@page:first {
background: none;
}
body {
font-size:1em;
width:17cm;
font-family: 'Century Schoolbook';
}
#front-page-header {
page-break-inside: avoid;
}
#front-page-header table {
width: 100%;
}
#top-logo {
width: 5cm;
width: 5cm;
border: solid 1px black;
}
#office-info {
text-align: right;
vertical-align: middle;
}
h1 {
background: black;
color: white;
text-align: center;
font-size: 2.6em;
padding: 13px 0;
margin: 50px 0;
font-weight: bold;
}
h2 {
background: lightgrey;
font-size: 1.6em;
padding: 8px 0;
font-weight: bold;
}
.intro {
page-break-inside: avoid;
}
.party {
page-break-inside: avoid;
}
.part {
text-align: right;
}
.content h2 {
page-break-before: always;
}
.content h3 {
margin-top: 55px;
font-weight: bold;
font-size: 1.5em;
page-break-after: avoid;
}
.content p {
page-break-inside: avoid;
text-indent: 2em;
}
.content td p {
text-indent: 0;
text-align: left;
}
.provision {
page-break-inside: avoid;
}
p {
text-align: justify;
}
li {
margin: 16px 0;
}
.footer {
margin-top: 30px;
page-break-inside: avoid;
}
.mention {
margin: 0;
font-size: 0.9em;
}
.signatures {
width: 100%;
}
.signatures td {
vertical-align: top;
text-align: center;
height: 3cm;
}

View File

@@ -0,0 +1,69 @@
import uuid
from fastapi import Depends, HTTPException
from firm.core.routes import get_crud_router
from firm.core.depends import get_authed_tenant_registry
from firm.contract.models import Contract, ContractDraft, ContractDraftStatus, replace_variables_in_value, ContractFilters
from firm.contract.schemas import ContractCreate, ContractRead, ContractUpdate, ContractInit
from firm.entity.models import Entity
from firm.template.models import ProvisionTemplate
contract_router = get_crud_router(Contract, ContractCreate, ContractRead, ContractUpdate, ContractFilters)
del(contract_router.routes[4]) #delete
del(contract_router.routes[3]) #update
del(contract_router.routes[1]) #create
@contract_router.post("/", response_description="Contract Successfully created")
async def create(schema: ContractCreate, reg=Depends(get_authed_tenant_registry)) -> ContractRead:
await schema.validate_foreign_key(reg.db)
draft = await ContractDraft.get(reg.db, schema.draft_id)
if not draft:
raise HTTPException(status_code=404, detail=f"Contract draft not found!")
for v in draft.variables:
if not v.key or not v.value:
raise HTTPException(status_code=400, detail="Variable {} is invalid".format(v))
contract_dict = schema.model_dump()
del(contract_dict['draft_id'])
contract_dict['lawyer'] = reg.partner.model_dump()
contract_dict['name'] = draft.name
contract_dict['title'] = draft.title
parties = []
for p in draft.parties:
parties.append({
'entity': await Entity.get(reg.db, p.entity_id),
'part': p.part,
'representative': await Entity.get(reg.db, p.representative_id) if p.representative_id else None,
'signature_uuid': str(uuid.uuid4())
})
contract_dict['parties'] = parties
provisions = []
for p in draft.provisions:
p = p.provision
provision = await ProvisionTemplate.get(reg.db, p.provision_template_id) if p.type == "template" \
else p
provisions.append({
'title': replace_variables_in_value(draft.variables, provision.title),
'body': replace_variables_in_value(draft.variables, provision.body)
})
contract_dict['provisions'] = provisions
record = await Contract.create(reg.db, ContractInit(**contract_dict))
await draft.update_status(reg.db, ContractDraftStatus.published)
return ContractRead.from_model(record)
@contract_router.put("/{record_id}", response_description="")
async def update(record_id: str, contract_form: ContractUpdate, reg=Depends(get_authed_tenant_registry)) -> ContractRead:
raise HTTPException(status_code=400, detail="No modification on contract")

View File

@@ -0,0 +1,42 @@
from beanie import PydanticObjectId
from fastapi import HTTPException, Depends
from firm.core.routes import get_crud_router
from firm.core.depends import get_authed_tenant_registry
from firm.contract.models import ContractDraft, ContractDraftStatus, ContractDraftFilters
from firm.contract.schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate, ContractDraftFilters)
del(draft_router.routes[3]) #update route
del(draft_router.routes[1]) #post route
@draft_router.post("/", response_description="Contract Draft added to the database")
async def create(schema: ContractDraftCreate, reg=Depends(get_authed_tenant_registry)) -> ContractDraftRead:
await schema.validate_foreign_key(reg.db)
record = await ContractDraft.create(reg.db, schema)
await record.check_is_ready(reg.db)
return ContractDraftRead.from_model(record)
@draft_router.put("/{record_id}", response_description="Contract Draft record updated")
async def update(record_id: PydanticObjectId, schema: ContractDraftUpdate, reg=Depends(get_authed_tenant_registry)) -> ContractDraftRead:
record = await ContractDraft.get(reg.db, record_id)
if not record:
raise HTTPException(
status_code=404,
detail="Contract Draft record not found!"
)
if record.status == ContractDraftStatus.published:
raise HTTPException(
status_code=400,
detail="Contract Draft has already been published"
)
record = await ContractDraft.update(reg.db, record, schema)
await record.check_is_ready(reg.db)
return ContractDraftRead.from_model(record)

View File

@@ -0,0 +1,35 @@
from fastapi import Depends, HTTPException, File, UploadFile, APIRouter
import shutil
from uuid import UUID
from firm.contract.models import Contract, Party
from firm.core.depends import get_tenant_registry
signature_router = APIRouter()
@signature_router.get("/{signature_id}", response_description="")
async def get_signature(signature_id: UUID, reg=Depends(get_tenant_registry)) -> Party:
contract = await Contract.find_by_signature_id(reg.db, signature_id)
signature = contract.get_signature(signature_id)
return signature
@signature_router.post("/{signature_id}", response_description="")
async def affix_signature(signature_id: UUID, signature_file: UploadFile = File(...), reg=Depends(get_tenant_registry)) -> bool:
contract = await Contract.find_by_signature_id(reg.db, signature_id)
if not contract:
raise HTTPException(status_code=404, detail="Contract record not found!")
signature_index = contract.get_signature_index(signature_id)
signature = contract.parties[signature_index]
if signature.signature_affixed:
raise HTTPException(status_code=400, detail="Signature already affixed")
with open(f'media/signatures/{signature_id}.png', "wb") as buffer:
shutil.copyfileobj(signature_file.file, buffer)
await contract.affix_signature(reg.db, signature_index)
return True

View File

@@ -0,0 +1,84 @@
import datetime
from typing import List
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, ForeignKey
class ContractDraftRead(Reader, ContractDraft):
pass
class ContractDraftCreate(Writer):
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=[],
props={"display": "dictionary"},
title='Variables'
)
async def validate_foreign_key(self, db):
for p in self.parties:
if p.entity_id:
p.entity = await Entity.get(db, p.entity_id)
if p.entity is None:
raise ValueError
class ContractDraftUpdate(ContractDraftCreate):
pass
class ForeignEntityRead(BaseModel):
model_config = ConfigDict(title='Avocat')
label: str
class PartyRead(BaseModel):
model_config = ConfigDict(title='Partie')
signature_affixed: bool = Field(title='Signature apposée?')
signature_uuid: str = Field(props={"display": "signature-link"}, title="Lien vers signature")
part: str = Field(title='Rôle')
entity: ForeignEntityRead = Field(title='Client')
class ContractRead(Reader, Contract):
model_config = ConfigDict(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: PydanticObjectId = ForeignKey(resource="contracts/drafts", schema="ContractDraft")
class ContractInit(BaseModel):
date: datetime.date
location: str
lawyer: dict
name: str
title: str
parties: List[dict]
provisions: List[dict]
class ContractUpdate(BaseModel):
pass

View File

@@ -0,0 +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.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 = CurrentFirm.get_current(self.db)
def check_user(self, user):
for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm:
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=404, detail="This firm doesn't exist or you are not allowed to access it.")
return 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.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)
try:
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

@@ -0,0 +1,165 @@
from collections import defaultdict
from collections.abc import Callable, Mapping
from pydantic import ValidationInfo, field_validator
from typing import Any, Optional, Union
from fastapi_filter.base.filter import BaseFilterModel
_odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str, Any]]]] = {
"neq": lambda value: {"$ne": value},
"gt": lambda value: {"$gt": value},
"gte": lambda value: {"$gte": value},
"in": lambda value: {"$in": value},
"isnull": lambda value: None if value else {"$ne": None},
"lt": lambda value: {"$lt": value},
"lte": lambda value: {"$lte": value},
"not": lambda value: {"$ne": value},
"ne": lambda value: {"$ne": value},
"not_in": lambda value: {"$nin": value},
"nin": lambda value: {"$nin": value},
"like": lambda value: {"$regex": f".*{value}.*"},
"ilike": lambda value: {"$regex": f".*{value}.*", "$options": "i"},
"exists": lambda value: {"$exists": value},
}
class Filter(BaseFilterModel):
def sort(self):
if not self.ordering_values:
return None
sort = {}
for column in self.ordering_values:
direction = 1
if column[0] in ["+", "-"]:
if column[0] == "-":
direction = -1
column = column[1:]
sort[column] = direction
return sort
@field_validator("*", mode="before")
@classmethod
def split_str(
cls: type["BaseFilterModel"], value: Optional[str], field: ValidationInfo
) -> Optional[Union[list[str], str]]:
if (
field.field_name is not None
and (
field.field_name == cls.Constants.ordering_field_name
or field.field_name.endswith("__in")
or field.field_name.endswith("__nin")
)
and isinstance(value, str)
):
if not value:
# Empty string should return [] not ['']
return []
return list(value.split(","))
return value
def _get_filter_conditions(self, nesting_depth: int = 1) -> list[tuple[Mapping[str, Any], Mapping[str, Any]]]:
filter_conditions: list[tuple[Mapping[str, Any], Mapping[str, Any]]] = []
for field_name, value in self.filtering_fields:
field_value = getattr(self, field_name)
if isinstance(field_value, Filter):
if not field_value.model_dump(exclude_none=True, exclude_unset=True):
continue
filter_conditions.append(
(
{field_name: _odm_operator_transformer["neq"](None)},
{"fetch_links": True, "nesting_depth": nesting_depth},
)
)
for part, part_options in field_value._get_filter_conditions(nesting_depth=nesting_depth + 1): # noqa: SLF001
for sub_field_name, sub_value in part.items():
filter_conditions.append(
(
{f"{field_name}.{sub_field_name}": sub_value},
{"fetch_links": True, "nesting_depth": nesting_depth, **part_options},
)
)
elif "__" in field_name:
stripped_field_name, operator = field_name.split("__")
search_criteria = _odm_operator_transformer[operator](value)
filter_conditions.append(({stripped_field_name: search_criteria}, {}))
elif field_name == self.Constants.search_field_name and hasattr(self.Constants, "search_model_fields"):
search_conditions = [
{search_field: _odm_operator_transformer["ilike"](value)}
for search_field in self.Constants.search_model_fields
]
filter_conditions.append(({"$or": search_conditions}, {}))
else:
filter_conditions.append(({field_name: value}, {}))
return filter_conditions
def filter(self, query):
data = self._get_filter_conditions()
for filter_condition, filter_kwargs in data:
for field_name, value in filter_condition.items():
if field_name in query:
query[field_name] = query[field_name] | value
else:
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
search: Optional[str] = None
order_by: Optional[list[str]] = None
created_at__lte: Optional[str] = None
created_at__gte: Optional[str] = None
created_by__in: Optional[list[str]] = None
updated_at__lte: Optional[str] = None
updated_at__gte: Optional[str] = None
updated_by__in: Optional[list[str]] = None

View File

@@ -0,0 +1,147 @@
from datetime import datetime, UTC
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")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), nullable=False, title="Modifié le")
updated_by: Optional[PydanticObjectId] = Field(default=None, title="Modifié par")
@property
def _id(self):
return self.id
@computed_field(title="Label")
def label(self) -> str:
return self.compute_label()
def compute_label(self) -> str:
return ""
@classmethod
def _collection_name(cls):
return cls.__name__
@classmethod
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.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)
return await cls.get(db, result.inserted_id)
@classmethod
def find(cls, db, filters):
return {
"collection": cls._get_collection(db),
"query_filter": filters.filter({}),
"sort": filters.sort(),
}
@classmethod
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):
document = await cls._get_collection(db).find_one({"_id": model_id})
if not document:
return None
document["id"] = document.pop("_id")
return cls.model_validate(document)
@classmethod
async def update(cls, db, model, update_schema):
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)
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": {
"type": "textarea",
"props": {
"placeholder": "Leaving this field empty will cause formData property to be `null`",
"rows": kwargs['size'] if 'size' in kwargs else 10
}
}
}
return Field(*args, **kwargs)
def RichtextMultiline(*args, **kwargs):
if 'props' not in kwargs:
kwargs['props'] = {}
kwargs['props']['richtext'] = True
kwargs['props']['multiline'] = True
return Field(*args, **kwargs)
def RichtextSingleline(*args, **kwargs):
if 'props' not in kwargs:
kwargs['props'] = {}
kwargs['props']['richtext'] = True
kwargs['props']['multiline'] = False
return Field(*args, **kwargs)
def ForeignKey(resource, schema, displayed_fields=None, *args, **kwargs):
kwargs["foreignKey"] = {
"reference": {
"resource": resource,
"schema": schema,
}
}
if displayed_fields:
kwargs["foreignKey"]["reference"]["displayedFields"] = displayed_fields
return Field(*args, **kwargs)
class DictionaryEntry(BaseModel):
key: str
value: str = ""

View File

@@ -0,0 +1,64 @@
from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException, Depends
from fastapi_filter import FilterDepends
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.motor import paginate
from firm.core.depends import get_authed_tenant_registry
from firm.core.models import CrudDocument
from firm.core.schemas import Writer, Reader
def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reader, model_update: Writer, model_filter):
model_name = model.__name__
router = APIRouter()
@router.get("/", response_model=Page[model_read], response_description=f"{model_name} records retrieved")
async def read_list(filters: model_filter=FilterDepends(model_filter), reg=Depends(get_authed_tenant_registry)) -> Page[model_read]:
return await paginate(**model.find(reg.db, filters))
@router.post("/", response_description=f"{model_name} added to the database")
async def create(schema: model_create, reg=Depends(get_authed_tenant_registry)) -> model_read:
await schema.validate_foreign_key(reg.db)
record = await model.create(reg.db, schema)
return model_read.from_model(record)
@router.get("/{record_id}", response_description=f"{model_name} record retrieved")
async def read_one(record_id: PydanticObjectId, reg=Depends(get_authed_tenant_registry)) -> model_read:
record = await model.get(reg.db, record_id)
if not record:
raise HTTPException(
status_code=404,
detail=f"{model_name} record not found!"
)
return model_read.from_model(record)
@router.put("/{record_id}", response_description=f"{model_name} record updated")
async def update(record_id: PydanticObjectId, schema: model_update, reg=Depends(get_authed_tenant_registry)) -> model_read:
record = await model.get(reg.db, record_id)
if not record:
raise HTTPException(
status_code=404,
detail=f"{model_name} record not found!"
)
record = await model.update(reg.db, record, schema)
return model_read.from_model(record)
@router.delete("/{record_id}", response_description=f"{model_name} record deleted from the database")
async def delete(record_id: PydanticObjectId, reg=Depends(get_authed_tenant_registry)) -> dict:
record = await model.get(reg.db, record_id)
if not record:
raise HTTPException(
status_code=404,
detail=f"{model_name} record not found!"
)
await model.delete(reg.db, record)
return {
"message": f"{model_name} deleted successfully"
}
add_pagination(router)
return router

View File

@@ -0,0 +1,20 @@
from typing import Optional
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
class Reader(BaseModel):
id: Optional[PydanticObjectId] = Field(validation_alias="_id")
created_by: PydanticObjectId = Field(title="Créé par")
updated_by: PydanticObjectId = Field(title="Modifié par")
@classmethod
def from_model(cls, model):
schema = cls.model_validate(model, from_attributes=True)
return schema
class Writer(BaseModel):
async def validate_foreign_key(self, db):
pass

View File

@@ -0,0 +1,77 @@
from typing import Any, Optional
from beanie import PydanticObjectId
from pydantic import Field, BaseModel
from firm.core.models import CrudDocument, CrudDocumentConfig
from firm.core.schemas import Writer, Reader
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
class CurrentFirm(CrudDocument):
instance: str = Field()
firm: str = Field()
entity_id: PydanticObjectId = Field()
primary_color: str = Field()
secondary_color: str = Field()
def __eq__(self, other: Any) -> bool:
if isinstance(other, dict):
return self.instance == other["instance"] and self.firm == other["firm"]
return super().__eq__(other)
def compute_label(self) -> str:
return f"{self.instance} / {self.firm}"
@classmethod
async def get_current(cls, db):
document = await cls._get_collection(db).find_one({})
if not document:
return None
document["id"] = document.pop("_id")
return cls.model_validate(document)
class CurrentFirmSchemaRead(BaseModel):
id: Optional[PydanticObjectId]
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, partner_list):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner, partner_list=partner_list)
return schema
class CurrentFirmSchemaCreate(Writer):
corporation: EntityCorporationCreate = Field(title="Informations sur la firme")
owner: EntityIndividualCreate = Field(title="Informations sur le dirigeant")
position: str = Field(title="Poste")
primary_color: str = Field()
secondary_color: str = Field()
class CurrentFirmSchemaUpdate(Writer):
pass
class Partner(CrudDocument):
document_config = CrudDocumentConfig(
indexes=["user_id", "entity_id"],
)
user_id: PydanticObjectId = Field()
entity_id: PydanticObjectId = Field()
@classmethod
async def get_by_user_id(cls, db, user_id):
document = await cls._get_collection(db).find_one({"user_id": str(user_id)})
if not document:
return None
document["id"] = document.pop("_id")
return cls.model_validate(document)

View File

@@ -0,0 +1,55 @@
from fastapi import APIRouter, Depends
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
from firm.current_firm import CurrentFirm, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.entity.models import Entity, Employee
from firm.entity.schemas import EntityRead
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 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)
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:
owner_entity = await Entity.create(reg.db, schema.owner)
await Partner.create(reg.db, Partner(user_id=reg.user.id, entity_id=owner_entity.id))
corporation_schema = schema.corporation
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
corp = await Entity.create(reg.db, corporation_schema)
firm = await CurrentFirm.create(reg.db, CurrentFirm(
instance=reg.instance,
firm=reg.firm,
entity_id=corp.id,
primary_color=schema.primary_color,
secondary_color=schema.secondary_color,
))
await Partner.create(Partner(user_id=reg.user.id, entity_id=owner_entity.id))
return CurrentFirmSchemaRead.from_model_and_entities(
firm,
EntityRead.from_model(corp),
EntityRead.from_model(owner_entity),
[EntityRead.from_model(owner_entity)]
)
@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 CurrentFirm.get_current(reg.db)
document = await CurrentFirm.update(reg.db, document, schema)
return CurrentFirmSchemaRead.from_model(document)

18
api/rpk-api/firm/db.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import motor.motor_asyncio
MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME")
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD")
DATABASE_URL = f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@mongo:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
async def stop_db():
client.close()
def get_db_client():
yield client

View File

@@ -0,0 +1 @@
from firm.entity.routes import router as entity_router

View File

@@ -0,0 +1,97 @@
from datetime import date
from enum import Enum
from typing import List, Literal, Optional
from fastapi_filter import FilterDepends, with_prefix
from pydantic import Field, BaseModel, ConfigDict
from beanie import PydanticObjectId
from firm.core.models import CrudDocument, ForeignKey, CrudDocumentConfig
from firm.core.filter import Filter, FilterSchema
class EntityType(BaseModel):
@property
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: 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},
title="Surnoms"
)
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
place_of_birth: Optional[str] = Field(default="", title='Lieu de naissance')
@property
def label(self) -> str:
# if len(self.surnames) > 0:
# return f"{self.firstname} \"{self.surnames[0]}\" {self.lastname}"
return f"{self.firstname} {self.lastname}"
class Employee(BaseModel):
model_config = ConfigDict(title='Fiche Employé')
position: str = Field(title='Poste')
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", title='Employé')
class Corporation(EntityType):
model_config = ConfigDict(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):
model_config = ConfigDict(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')
def compute_label(self) -> str:
if not self.entity_data:
return ""
return self.entity_data.label
class EntityDataFilter(Filter):
type__in: Optional[list[str]] = None
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

@@ -0,0 +1,5 @@
from firm.core.routes import get_crud_router
from firm.entity.models import Entity, EntityFilters
from firm.entity.schemas import EntityCreate, EntityRead, EntityUpdate
router = get_crud_router(Entity, EntityCreate, EntityRead, EntityUpdate, EntityFilters)

View File

@@ -0,0 +1,22 @@
from pydantic import Field, ConfigDict
from firm.entity.models import Entity, Institution, Individual, Corporation
from firm.core.schemas import Writer, Reader
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 EntityIndividualCreate(EntityCreate):
entity_data: Individual
class EntityCorporationCreate(EntityCreate):
entity_data: Corporation
class EntityUpdate(EntityCreate):
pass

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

@@ -0,0 +1,9 @@
from fastapi import APIRouter
from firm.template.routes_contract import router as contract_router
from firm.template.routes_provision import router as provision_router
template_router = APIRouter()
template_router.include_router(provision_router, prefix="/provisions", )
template_router.include_router(contract_router, prefix="/contracts", )

View File

@@ -0,0 +1,77 @@
from typing import List, Optional
from html import unescape
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry, ForeignKey
from firm.core.filter import Filter, FilterSchema
class PartyTemplate(BaseModel):
model_config = ConfigDict(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):
"""Remove html tags from a string"""
import re
clean = re.compile('<.*?>')
return re.sub(clean, '', text)
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")
body: str = RichtextMultiline(title="Corps")
def compute_label(self) -> str:
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
class ProvisionTemplateReference(BaseModel):
model_config = ConfigDict(title="Clause")
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"ProvisionTemplate",
['title', 'body'],
props={"parametrized": True},
title="Template de 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=[], 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 ContractTemplateFilters(FilterSchema):
class Constants(Filter.Constants):
model = ContractTemplate
search_model_fields = ["label"]
class ProvisionTemplateFilters(FilterSchema):
class Constants(Filter.Constants):
model = ProvisionTemplate
search_model_fields = ["label"]

View File

@@ -0,0 +1,5 @@
from firm.core.routes import get_crud_router
from firm.template.models import ContractTemplate, ContractTemplateFilters
from firm.template.schemas import ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate
router = get_crud_router(ContractTemplate, ContractTemplateCreate, ContractTemplateRead, ContractTemplateUpdate, ContractTemplateFilters)

View File

@@ -0,0 +1,5 @@
from firm.core.routes import get_crud_router
from firm.template.models import ProvisionTemplate, ProvisionTemplateFilters
from firm.template.schemas import ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate
router = get_crud_router(ProvisionTemplate, ProvisionTemplateCreate, ProvisionTemplateRead, ProvisionTemplateUpdate, ProvisionTemplateFilters)

View File

@@ -0,0 +1,51 @@
from pydantic import Field, ConfigDict
from typing import List
from firm.template.models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry
from firm.core.schemas import Writer, Reader
from firm.core.models import RichtextMultiline, RichtextSingleline
class ContractTemplateRead(Reader, ContractTemplate):
pass
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=[],
props={"items_per_row": "2"},
title="Parties")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items_per_row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field(
default=[],
props={"display": "dictionary", "required": False},
title="Variables"
)
class ContractTemplateUpdate(ContractTemplateCreate):
pass
class ProvisionTemplateRead(Reader, ProvisionTemplate):
pass
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 ProvisionTemplateUpdate(ProvisionTemplateCreate):
pass

View File

@@ -1,19 +1,16 @@
import os from fastapi import APIRouter
from beanie import init_beanie from hub.auth import auth_router, register_router, password_router, verification_router, users_router, \
from motor.motor_asyncio import AsyncIOMotorClient google_oauth_router, discord_oauth_router
from hub.firm.routes import router as firm_router
from hub.user import User hub_router = APIRouter()
from hub.auth import AccessToken
MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME") hub_router.include_router(register_router, tags=["Auth"], )
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD") hub_router.include_router(auth_router, prefix="/auth", tags=["Auth"], )
hub_router.include_router(google_oauth_router, prefix="/auth/google", tags=["Auth"])
DATABASE_URL = f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@mongo:27017" hub_router.include_router(discord_oauth_router, prefix="/auth/discord", tags=["Auth"])
hub_router.include_router(verification_router, prefix="/auth/verification", tags=["Auth"], )
async def init_db(): hub_router.include_router(users_router, prefix="/users", tags=["Users"], )
client = AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard") hub_router.include_router(password_router, prefix="/users", tags=["Users"], )
hub_router.include_router(firm_router, prefix="/users/firms", tags=["Users"], )
await init_beanie(database=client.hub,
document_models=[User, AccessToken],
allow_index_dropping=True)

View File

@@ -1,16 +1,19 @@
import os import os
import uuid from typing import Any
from beanie import PydanticObjectId from beanie import PydanticObjectId, Document
from fastapi import Depends from fastapi import Depends, Response, status, APIRouter
from fastapi_users import UUIDIDMixin, BaseUserManager, FastAPIUsers, schemas from fastapi_users import BaseUserManager, FastAPIUsers, schemas, models
from fastapi_users.authentication import AuthenticationBackend, BearerTransport, CookieTransport from fastapi_users.authentication import AuthenticationBackend, CookieTransport, Strategy
from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy
from fastapi_users_db_beanie.access_token import BeanieBaseAccessTokenDocument, BeanieAccessTokenDatabase from fastapi_users_db_beanie.access_token import BeanieBaseAccessToken, BeanieAccessTokenDatabase
from fastapi_users.openapi import OpenAPIResponseType
from httpx_oauth.clients.google import GoogleOAuth2 from httpx_oauth.clients.google import GoogleOAuth2
from httpx_oauth.clients.discord import DiscordOAuth2 from httpx_oauth.clients.discord import DiscordOAuth2
from starlette.responses import JSONResponse, RedirectResponse
from hub.user import User, get_user_db from hub.user import User, get_user_db
from hub.user.schemas import UserSchema, UserUpdateSchema
SECRET = os.getenv("FASTAPI_USERS_SECRET") SECRET = os.getenv("FASTAPI_USERS_SECRET")
@@ -19,11 +22,8 @@ discord_oauth_client = DiscordOAuth2(os.getenv("DISCORD_CLIENT_ID"), os.getenv("
TOKEN_LIFETIME = 3600 TOKEN_LIFETIME = 3600
# bearer_transport = BearerTransport(tokenUrl="auth/login")
cookie_transport = CookieTransport(cookie_name="rpkapiusersauth")
class AccessToken(BeanieBaseAccessToken, Document):
class AccessToken(BeanieBaseAccessTokenDocument):
pass pass
async def get_access_token_db(): async def get_access_token_db():
@@ -36,24 +36,61 @@ def get_database_strategy(
return DatabaseStrategy(access_token_db, lifetime_seconds=TOKEN_LIFETIME) return DatabaseStrategy(access_token_db, lifetime_seconds=TOKEN_LIFETIME)
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): class UserManager(BaseUserManager[User, PydanticObjectId]):
reset_password_token_secret = SECRET reset_password_token_secret = SECRET
verification_token_secret = SECRET verification_token_secret = SECRET
def parse_id(self, value: Any) -> models.ID:
return str(value)
async def get_user_manager(user_db=Depends(get_user_db)): async def get_user_manager(user_db=Depends(get_user_db)):
yield UserManager(user_db) yield UserManager(user_db)
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, class CookieTransportMe(CookieTransport):
) async def get_login_me_response(self, token: str, user) -> Response:
user_schema = UserSchema(**user.model_dump())
response = JSONResponse(status_code=status.HTTP_200_OK, content=user_schema.model_dump(mode="json"))
return self._set_login_cookie(response, token)
@staticmethod
def get_openapi_login_responses_success() -> OpenAPIResponseType:
return {status.HTTP_200_OK: {"model": UserSchema}}
class AuthenticationBackendMe(AuthenticationBackend):
async def login(self, strategy: Strategy[models.UP, models.ID], user: models.UP) -> Response:
token = await strategy.write_token(user)
return await self.transport.get_login_me_response(token, user)
class CookieTransportOauth(CookieTransport):
async def get_login_response(self, token: str) -> Response:
response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
return self._set_login_cookie(response, token)
@staticmethod
def get_openapi_login_responses_success() -> OpenAPIResponseType:
return {status.HTTP_301_MOVED_PERMANENTLY: {"model": None}}
cookie_transport = CookieTransportMe(cookie_name="rpkapiusersauth")
auth_backend = AuthenticationBackendMe(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend]) fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend])
get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
auth_router = fastapi_users.get_auth_router(auth_backend, requires_verification=True) auth_router = fastapi_users.get_auth_router(auth_backend, requires_verification=True)
register_router = fastapi_users.get_register_router(schemas.BaseUser, schemas.BaseUserCreate) register_router = fastapi_users.get_register_router(UserSchema, schemas.BaseUserCreate)
password_router = fastapi_users.get_reset_password_router() password_router = fastapi_users.get_reset_password_router()
verification_router = fastapi_users.get_verify_router(schemas.BaseUser) verification_router = fastapi_users.get_verify_router(UserSchema)
users_router = fastapi_users.get_users_router(schemas.BaseUser, schemas.BaseUserUpdate) users_router = fastapi_users.get_users_router(UserSchema, UserUpdateSchema)
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET)
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET) cookie_transport = CookieTransportOauth(cookie_name="rpkapiusersauth")
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET, is_verified_by_default=True)

View File

@@ -0,0 +1,19 @@
from datetime import datetime
from beanie import Document, PydanticObjectId
from pydantic import Field, computed_field
class CrudDocument(Document):
_id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False, title="Créé le")
created_by: PydanticObjectId = Field()
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
updated_by: PydanticObjectId = Field()
@computed_field
def label(self) -> str:
return self.compute_label()
def compute_label(self) -> str:
return ""

21
api/rpk-api/hub/db.py Normal file
View File

@@ -0,0 +1,21 @@
import os
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from hub.user import User
from hub.auth import AccessToken
from hub.firm import Firm
MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME")
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD")
DATABASE_URL = f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@mongo:27017"
async def init_db():
client = AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard")
await init_beanie(database=client.hub,
document_models=[User, AccessToken, Firm],
allow_index_dropping=True)

View File

@@ -0,0 +1,33 @@
from beanie import PydanticObjectId
from pydantic import Field, BaseModel
from pymongo import IndexModel
from hub.core import CrudDocument
class Firm(CrudDocument):
instance: str = Field()
firm: str = Field()
owner: PydanticObjectId = Field()
@classmethod
def get_by_name(cls, instance, firm):
return cls.find_one({"instance": instance, "firm": firm})
def compute_label(self) -> str:
return f"{self.instance} / {self.firm}"
class Settings:
indexes = [
IndexModel(["instance", "firm"], unique=True),
]
class FirmRead(BaseModel):
instance: str = Field()
firm: str = Field()
class FirmCreate(FirmRead):
instance: str = Field(max_length=32, min_length=3, pattern="^[0-9a-z-]+$")
firm: str = Field(max_length=32, min_length=3, pattern="^[0-9a-z-]+$")
class FirmUpdate(BaseModel):
owner: PydanticObjectId = Field()

View File

@@ -0,0 +1,69 @@
from fastapi import APIRouter, Depends, HTTPException
from hub.auth import get_current_user
from hub.firm import Firm, FirmRead, FirmCreate, FirmUpdate
router = APIRouter()
@router.get("/", response_model=list[FirmRead], response_description="List of firms owned by the current user")
async def read_list(user=Depends(get_current_user)) -> list[FirmRead]:
return await Firm.find({ "owner": user.id}).to_list()
@router.post("/", response_description="Firm added to the database")
async def create(item: FirmCreate, user=Depends(get_current_user)) -> FirmRead:
exists = await Firm.get_by_name(item.instance, item.firm)
if exists:
raise HTTPException(status_code=400, detail="Firm already exists")
record = Firm(created_by=user.id, updated_by=user.id, owner=user.id, **item.model_dump())
o = await record.create()
user.firms.append({"instance": item.instance, "firm": item.firm})
await user.save()
return FirmRead(**o.model_dump())
@router.get("/{instance}/{firm}", response_description="Firm retrieved")
async def read_id(instance: str, firm: str, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Item not found")
return FirmRead(**item.model_dump())
@router.put("/{instance}/{firm}", response_description="Firm updated")
async def update(instance: str, firm: str, req: FirmUpdate, user=Depends(get_current_user)) -> FirmRead:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Item not found")
if item.owner != user.id:
raise HTTPException(
status_code=403,
detail="Insufficient credentials to modify Firm"
)
req = {k: v for k, v in req.model_dump().items() if v is not None}
update_query = {"$set": {
field: value for field, value in req.items()
}}
await item.update(update_query)
return FirmRead(**item.dict())
@router.delete("/{instance}/{firm}", response_description="Firm deleted from the database")
async def delete(instance: str, firm: str, user=Depends(get_current_user)) -> dict:
item = await Firm.get_by_name(instance, firm)
if not item or not user.belong_to(item) not in user.firms:
raise HTTPException(status_code=404, detail="Firm not found")
if item.owner != user.id:
raise HTTPException(
status_code=403,
detail="Insufficient credentials delete Firm"
)
await item.delete()
return {
"message": "Firm deleted successfully"
}

View File

@@ -1,19 +1,20 @@
from fastapi import Depends from beanie import Document
from fastapi_users_db_beanie import BaseOAuthAccount, BeanieUserDatabase, BeanieBaseUserDocument from fastapi_users_db_beanie import BaseOAuthAccount, BeanieUserDatabase, BeanieBaseUser
from pydantic import Field from pydantic import Field
from hub.firm import FirmRead
from hub.user.schemas import UserSchema, UserUpdateSchema
class OAuthAccount(BaseOAuthAccount): class OAuthAccount(BaseOAuthAccount):
pass pass
class User(BeanieBaseUserDocument): class User(BeanieBaseUser, Document):
oauth_accounts: list[OAuthAccount] = Field(default_factory=list) oauth_accounts: list[OAuthAccount] = Field(default_factory=list)
firms: list[FirmRead] = Field(default_factory=list)
class UserDatabase(BeanieUserDatabase): class UserDatabase(BeanieUserDatabase):
pass pass
async def get_user_db(): async def get_user_db():
yield UserDatabase(User, OAuthAccount) yield UserDatabase(User, OAuthAccount)
async def get_user_manager(user_db=Depends(get_user_db)):
yield UserManager(user_db)

View File

@@ -0,0 +1,18 @@
from beanie import PydanticObjectId
from fastapi_users.schemas import BaseUser, BaseUserUpdate
from pydantic import Field
from hub.firm import FirmRead
class UserSchema(BaseUser[PydanticObjectId]):
firms: list[FirmRead] = Field()
def belongs_to(self, firm):
for f in self.firms:
if f.instance == firm.instance and f.firm == firm.firm :
return True
return False
class UserUpdateSchema(BaseUserUpdate):
pass

View File

@@ -1,10 +1,12 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from hub import init_db as hub_init_db from hub import hub_router
from hub.auth import auth_router, register_router, password_router, verification_router, users_router, \ from hub.db import init_db as hub_init_db
google_oauth_router, discord_oauth_router
from firm import firm_router
from firm.init_db import init_db as firm_init_db
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn
@@ -14,17 +16,12 @@ if __name__ == '__main__':
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
await hub_init_db() await hub_init_db()
await firm_init_db()
# create_db_and_tables() # create_db_and_tables()
# create_admin_user() # create_admin_user()
yield yield
# do something before end # do something before end
app = FastAPI(root_path="/api/v1", lifespan=lifespan) app = FastAPI(root_path="/api/v1", lifespan=lifespan)
app.include_router(hub_router, prefix="/hub")
app.include_router(register_router, tags=["Auth"], ) app.include_router(firm_router, prefix="/firm")
app.include_router(auth_router, prefix="/auth", tags=["Auth"], )
app.include_router(google_oauth_router, prefix="/auth/google", tags=["Auth"])
app.include_router(discord_oauth_router, prefix="/auth/discord", tags=["Auth"])
app.include_router(verification_router, prefix="/auth/verification", tags=["Auth"], )
app.include_router(users_router, prefix="/users", tags=["Users"], )
app.include_router(password_router, prefix="/users", tags=["Users"], )

View File

@@ -12,9 +12,9 @@ services:
- ./api/media:/app/media - ./api/media:/app/media
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.back.entrypoints=web" - "traefik.http.routers.api.entrypoints=web"
- "traefik.http.routers.back.rule=PathPrefix(`/api/v1/`)" - "traefik.http.routers.api.rule=PathPrefix(`/api/v1/`)"
- "traefik.http.services.back.loadbalancer.server.port=8000" - "traefik.http.services.api.loadbalancer.server.port=8000"
gui: gui:
build: build:
@@ -22,15 +22,28 @@ services:
image: roleplay-contracts-gui-dev image: roleplay-contracts-gui-dev
restart: always restart: always
ports: ports:
- "4200:4200" - "5173:5173"
volumes: volumes:
- ./gui/rpk-gui/src:/app/src - ./gui/rpk-gui/src:/app/src
- ./gui/rpk-gui/public:/app/public - ./gui/rpk-gui/public:/app/public
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.front.entrypoints=web" - "traefik.http.routers.gui.entrypoints=web"
- "traefik.http.routers.front.rule=PathPrefix(`/`)" - "traefik.http.routers.gui.rule=PathPrefix(`/`)"
- "traefik.http.services.front.loadbalancer.server.port=4200" - "traefik.http.services.gui.loadbalancer.server.port=5173"
i18n:
build:
context: ./i18n
restart: always
volumes:
- ./i18n/app/src:/app/src
- ./gui/rpk-gui/public:/app/public
labels:
- "traefik.enable=true"
- "traefik.http.routers.i18n.entrypoints=web"
- "traefik.http.routers.i18n.rule=PathPrefix(`/locales/add`)"
- "traefik.http.services.i18n.loadbalancer.server.port=8100"
proxy: proxy:
image: traefik:latest image: traefik:latest

View File

@@ -1,13 +1,13 @@
FROM node:lts-alpine FROM node:lts-alpine
WORKDIR /app WORKDIR /app
RUN npm install -g @angular/cli http-server RUN npm install -g http-server
COPY rpk-gui/package*.json ./ COPY rpk-gui/package*.json ./
RUN npm install RUN npm install
COPY rpk-gui/ . COPY rpk-gui/ .
RUN npm run build RUN npm run build
EXPOSE 4200 EXPOSE 5173
CMD [ "npm", "run", "ng", "serve", "--", "--host", "0.0.0.0", "--disable-host-check" ] CMD [ "npm", "run", "dev", "--", "--host"]

16
gui/rpk-gui/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,16 @@
/* eslint-env node */
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
},
};

View File

@@ -1,7 +1,8 @@
# Logs # Logs
logs logs
*.log _.log
npm-debug.log* npm-debug.log_
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
@@ -10,15 +11,16 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
*.local \*.local
# Editor directories and files # Editor directories and files
.vscode/*
.vscode/_
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.DS_Store .DS_Store
*.suo _.suo
*.ntvs* _.ntvs_
*.njsproj _.njsproj
*.sln _.sln
*.sw? \*.sw?

1
gui/rpk-gui/.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

37
gui/rpk-gui/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# This Dockerfile uses `serve` npm package to serve the static files with node process.
# You can find the Dockerfile for nginx in the following link:
# https://github.com/refinedev/dockerfiles/blob/main/vite/Dockerfile.nginx
FROM refinedev/node:18 AS base
FROM base as deps
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base as builder
ENV NODE_ENV production
COPY --from=deps /app/refine/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base as runner
ENV NODE_ENV production
RUN npm install -g serve
COPY --from=builder /app/refine/dist ./
USER refine
CMD ["serve"]

49
gui/rpk-gui/README.MD Normal file
View File

@@ -0,0 +1,49 @@
# rpk-gui
<div align="center" style="margin: 30px;">
<a href="https://refine.dev">
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
</a>
</div>
<br/>
This [Refine](https://github.com/refinedev/refine) project was generated with [create refine-app](https://github.com/refinedev/refine/tree/master/packages/create-refine-app).
## Getting Started
A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility ✨
Refine's hooks and components simplifies the development process and eliminates the repetitive tasks by providing industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, state management, and i18n.
## Available Scripts
### Running the development server.
```bash
npm run dev
```
### Building for production.
```bash
npm run build
```
### Running the production server.
```bash
npm run start
```
## Learn More
To learn more about **Refine**, please check out the [Documentation](https://refine.dev/docs)
- **REST Data Provider** [Docs](https://refine.dev/docs/core/providers/data-provider/#overview)
- **Material UI** [Docs](https://refine.dev/docs/ui-frameworks/mui/tutorial/)
- **React Router** [Docs](https://refine.dev/docs/core/providers/router-provider/)
- **Custom Auth Provider** [Docs](https://refine.dev/docs/core/providers/auth-provider/)
## License
MIT

View File

@@ -1,54 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -1,13 +1,41 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vite + React + TS</title> <meta name="theme-color" content="#000000" />
<meta
name="description"
content="refine | Build your React-based CRUD applications, without constraints."
/>
<meta
data-rh="true"
property="og:image"
content="https://refine.dev/img/refine_social.png"
/>
<meta
data-rh="true"
name="twitter:image"
content="https://refine.dev/img/refine_social.png"
/>
<title>
Refine - Build your React-based CRUD applications, without constraints.
</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm dev` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,80 @@
{ {
"name": "rpk-gui", "name": "rpk-gui",
"version": "0.1.0",
"private": true, "private": true,
"version": "0.0.0",
"type": "module", "type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": { "dependencies": {
"@rjsf/mui": "^5.24.8", "@emotion/react": "^11.8.2",
"@tanstack/react-query": "^5.71.3", "@emotion/styled": "^11.8.1",
"better-auth": "^1.2.5", "@mui/icons-material": "^6.1.6",
"react": "^19.0.0", "@mui/lab": "^6.0.0-beta.14",
"react-dom": "^19.0.0", "@mui/material": "^6.1.7",
"react-router": "^7.4.1" "@mui/x-data-grid": "^7.22.2",
"@mui/x-date-pickers": "^8.3.0",
"@refinedev/cli": "^2.16.21",
"@refinedev/core": "^4.47.1",
"@refinedev/devtools": "^1.1.32",
"@refinedev/kbar": "^1.3.6",
"@refinedev/mui": "^6.0.0",
"@refinedev/react-hook-form": "^4.8.14",
"@refinedev/react-router": "^1.0.0",
"@refinedev/simple-rest": "^5.0.1",
"@rjsf/core": "^5.24.1",
"@rjsf/mui": "^5.24.1",
"@rjsf/utils": "^5.24.1",
"@rjsf/validator-ajv8": "^5.24.1",
"@tiptap/extension-bubble-menu": "^2.11.7",
"@tiptap/extension-table": "^2.11.7",
"@tiptap/extension-table-cell": "^2.11.7",
"@tiptap/extension-table-header": "^2.11.7",
"@tiptap/extension-table-row": "^2.11.7",
"@tiptap/extension-text-align": "^2.11.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",
"lodash": "^4.17.21",
"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"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@types/lodash": "^4.17.16",
"@types/react": "^19.1.0", "@types/node": "^18.16.2",
"@types/react-dom": "^19.1.1", "@types/react": "^18.0.0",
"@vitejs/plugin-react": "^4.3.4", "@types/react-dom": "^18.0.0",
"eslint": "^9.21.0", "@typescript-eslint/eslint-plugin": "^5.57.1",
"eslint-plugin-react-hooks": "^5.1.0", "@typescript-eslint/parser": "^5.57.1",
"eslint-plugin-react-refresh": "^0.4.19", "@vitejs/plugin-react": "^4.0.0",
"globals": "^15.15.0", "eslint": "^8.38.0",
"typescript": "~5.7.2", "eslint-plugin-react-hooks": "^4.6.0",
"typescript-eslint": "^8.24.1", "eslint-plugin-react-refresh": "^0.3.4",
"vite": "^6.2.0" "typescript": "^5.4.2",
"vite": "^6.2.5"
},
"scripts": {
"dev": "refine dev --devtools=false",
"build": "tsc && refine build",
"start": "refine start",
"refine": "refine"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,164 @@
{
"pages": {
"login": {
"title": "Melden Sie sich bei Ihrem Konto an",
"signin": "Einloggen",
"signup": "Anmelden",
"divider": "oder",
"fields": {
"email": "Email",
"password": "Passwort"
},
"oauth": {
"google": "Einloggen mit Google",
"discord": "Einloggen mit Discord"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Anmeldung",
"forgotPassword": "Passwort vergessen?",
"noAccount": "Sie haben kein Konto?",
"rememberMe": "Erinnere dich an mich"
}
},
"forgotPassword": {
"title": "Haben Sie Ihr Passwort vergessen?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich"
},
"buttons": {
"submit": "Anweisungen zum Zurücksetzen senden"
}
},
"register": {
"title": "Registrieren Sie sich für Ihr Konto",
"fields": {
"email": "Email",
"password": "Passwort"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Registrieren",
"haveAccount": "Ein Konto haben?"
}
},
"updatePassword": {
"title": "Kennwort aktualisieren",
"fields": {
"password": "Neues Passwort",
"confirmPassword": "Bestätige neues Passwort"
},
"errors": {
"confirmPasswordNotMatch": "Passwörter stimmen nicht überein",
"requiredPassword": "Passwort wird benötigt",
"requiredConfirmPassword": "Das Feld „Passwort bestätigen“ ist erforderlich"
},
"buttons": {
"submit": "Aktualisieren"
}
},
"error": {
"info": "Sie haben vergessen, {{action}} component zu {{resource}} hinzufügen.",
"404": "Leider existiert diese Seite nicht.",
"resource404": "Haben Sie die {{resource}} resource erstellt?",
"backHome": "Zurück"
}
},
"actions": {
"list": "Aufführen",
"create": "Erstellen",
"edit": "Bearbeiten",
"show": "Zeigen"
},
"buttons": {
"create": "Erstellen",
"save": "Speichern",
"logout": "Abmelden",
"delete": "Löschen",
"edit": "Bearbeiten",
"cancel": "Abbrechen",
"confirm": "Sicher?",
"filter": "Filter",
"clear": "Löschen",
"refresh": "Erneuern",
"show": "Zeigen",
"undo": "Undo",
"import": "Importieren",
"clone": "Klon",
"notAccessTitle": "Sie haben keine zugriffsberechtigung"
},
"warnWhenUnsavedChanges": "Nicht gespeicherte Änderungen werden nicht übernommen.",
"notifications": {
"success": "Erfolg",
"error": "Fehler (status code: {{statusCode}})",
"undoable": "Sie haben {{seconds}} Sekunden Zeit für Undo.",
"createSuccess": "{{resource}} erfolgreich erstellt.",
"createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} erfolgreich gelöscht.",
"deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} erfolgreich bearbeitet.",
"editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert"
},
"loading": "Wird geladen",
"tags": {
"clone": "Klon"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Einträge",
"fields": {
"id": "Id",
"title": "Titel",
"category": "Kategorie",
"status": {
"title": "Status",
"published": "Veröffentlicht",
"draft": "Draft",
"rejected": "Abgelehnt"
},
"content": "Inhalh",
"createdAt": "Erstellt am"
},
"titles": {
"create": "Erstellen",
"edit": "Bearbeiten",
"list": "Einträge",
"show": "Eintrag zeigen"
}
},
"table": {
"actions": "Aktionen"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Beiträge | Refine",
"show": "#{{id}} Beitrag anzeigen | Refine",
"edit": "#{{id}} Beitrag bearbeiten | Refine",
"create": "Neuen Beitrag erstellen | Refine",
"clone": "#{{id}} Beitrag klonen | Refine"
}
},
"autoSave": {
"success": "gespeichert",
"error": "fehler beim automatischen speichern",
"loading": "speichern...",
"idle": "warten auf anderungen"
}
}

View File

@@ -0,0 +1,284 @@
{
"pages": {
"home": {
"title": "Home"
},
"login": {
"title": "Sign in to your account",
"signin": "Sign in",
"signup": "Sign up",
"divider": "or",
"fields": {
"email": "Email",
"password": "Password"
},
"oauth": {
"google": "Sign in with Google",
"discord": "Sign in with Discord"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Login",
"forgotPassword": "Forgot password?",
"noAccount": "Dont have an account?",
"rememberMe": "Remember me"
}
},
"forgotPassword": {
"title": "Forgot your password?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required"
},
"buttons": {
"submit": "Send reset instructions"
}
},
"register": {
"title": "Sign up for your account",
"fields": {
"email": "Email",
"password": "Password"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Register",
"haveAccount": "Have an account?"
}
},
"updatePassword": {
"title": "Update password",
"fields": {
"password": "New Password",
"confirmPassword": "Confirm new password"
},
"errors": {
"confirmPasswordNotMatch": "Passwords do not match",
"requiredPassword": "Password required",
"requiredConfirmPassword": "Confirm password is required"
},
"buttons": {
"submit": "Update"
}
},
"error": {
"404": "Sorry, the page you visited does not exist.",
"info": "You may have forgotten to add the {{action}} component to {{resource}} resource.",
"resource404": "Are you sure you have created the {{resource}} resource.",
"backHome": "Back Home"
}
},
"actions": {
"list": "List",
"create": "Create",
"edit": "Edit",
"show": "Show"
},
"buttons": {
"create": "Create",
"save": "Save",
"logout": "Logout",
"delete": "Delete",
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
"show": "Show",
"undo": "Undo",
"import": "Import",
"clone": "Clone",
"notAccessTitle": "You don't have permission to access"
},
"warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.",
"notifications": {
"success": "Successful",
"error": "Error (status code: {{statusCode}})",
"undoable": "You have {{seconds}} seconds to undo",
"createSuccess": "Successfully created {{resource}}",
"createError": "There was an error creating {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "Successfully deleted {{resource}}",
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
"editSuccess": "Successfully edited {{resource}}",
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
"importProgress": "Importing: {{processed}}/{{total}}"
},
"loading": "Loading",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "saved",
"error": "auto save failure",
"loading": "saving...",
"idle": "waiting for changes"
},
"undefined": {
"undefined": "No translation",
"titles": {
"list": "No translation"
}
},
"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",
"surnames": "Surname",
"day_of_birth": "Date of birth",
"firstname": "Firstname",
"place_of_birth": "Place of birth",
"middlename": "Middlename",
"resource_title": "Individual"
},
"corporation": {
"type": "Corporation",
"activity": "Activity",
"title": "Title",
"employees": "Employees",
"resource_title": "Corporation"
},
"employee": {
"position": "Position",
"entity_id": "Identity",
"resource_title": "Employee"
},
"institution": {
"type": "Institution",
"title": "Title",
"activity": "Activity",
"employees": "Employees",
"resource_title": "Institution"
},
"entity": {
"entity_data": "Informations",
"address": "Address",
"resource_title": "Entity",
"resource_plural": "Entities"
},
"provision_template": {
"name": "Name",
"title": "Title",
"body": "Body",
"resource_title": "Provision Template",
"resource_plural": "Provision Templates"
},
"contract_template": {
"name": "Name",
"title": "Title",
"provisions": "Provisions",
"parties": "Parties",
"variables": "Variables",
"resource_title": "Contract Template",
"resource_plural": "Contract Templates"
},
"party_template": {
"entity_id": "Entity",
"representative_id": "Representative",
"part": "Part",
"resource_title": "Party"
},
"provision_template_reference": {
"provision_template_id": "Provision Template",
"resource_title": "Provision Template"
},
"dictionary_entry": {
"key": "Variable",
"value": "Value",
"resource_title": "Variables"
},
"contract_draft": {
"name": "Name",
"title": "Title",
"parties": "Parties",
"provisions": "Provisions",
"variables": "Variables",
"resource_title": "Contract Draft",
"resource_plural": "Contract Drafts"
},
"draft_party": {
"entity_id": "Client",
"part": "Part",
"representative_id": "Representative",
"resource_title": "Party"
},
"contract_provision_template_reference": {
"provision_template_id": "Provision Template",
"type": "Provision Template",
"resource_title": "Provision Template"
},
"provision_genuine": {
"title": "Title",
"body": "Body",
"type": "Genuine Provision",
"resource_title": "Genuine Provision"
},
"draft_provision": {
"provision": "Provision",
"resource_title": "Provision"
},
"contract": {
"date": "Date",
"location": "Location",
"resource_title": "Contract",
"resource_plural": "Contracts",
"draft_id": "Draft"
}
}
}

View File

@@ -0,0 +1,284 @@
{
"pages": {
"home": {
"title": "Page d'accueil"
},
"login": {
"title": "Authentification",
"signin": "S'authentifier",
"signup": "Créer un compte",
"divider": "ou",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"oauth": {
"google": "S'authentifier avec Google",
"discord": "S'authentifier avec Discord"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "S'authentifier",
"forgotPassword": "Mot de passe oublié?",
"noAccount": "Vous n'avec pas de compte?",
"rememberMe": "Se souvenir de moi"
}
},
"forgotPassword": {
"title": "Mot de passe oublié?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "mail invalide",
"requiredEmail": "l'Email est obligatoire"
},
"buttons": {
"submit": "Envoyer les instructions de récupération"
}
},
"register": {
"title": "Création de compte",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "Créer un compte",
"haveAccount": "Vous avez déjà un compte?"
}
},
"updatePassword": {
"title": "Mise à jour du mot de passe",
"fields": {
"password": "Nouveau mot de passe",
"confirmPassword": "Confirmation"
},
"errors": {
"confirmPasswordNotMatch": "Les mots de passe ne correspondent pas",
"requiredPassword": "Le mot de passe est obligatoire",
"requiredConfirmPassword": "Vous devez confirmer votre mot de passe"
},
"buttons": {
"submit": "Mettre à jour"
}
},
"error": {
"404": "Cette page n'existe pas.",
"info": "Il manque l'action {{action}} component à la ressource {{resource}} .",
"resource404": "Cette page n'existe pas.",
"backHome": "Retour à l'accueil"
}
},
"actions": {
"list": "Liste",
"create": "Création",
"edit": "Édtion",
"show": "Voir"
},
"buttons": {
"create": "Créer",
"save": "Sauvegarder",
"logout": "Déconnexion",
"delete": "Supprimer",
"edit": "Modifier",
"cancel": "Annuler",
"confirm": "Êtes vous sur?",
"filter": "Filtrer",
"clear": "Effacer",
"refresh": "Rafraîchir",
"show": "Voir",
"undo": "Annuler",
"import": "Importer",
"clone": "Cloner",
"notAccessTitle": "Vous n'avez pas la permission d'accéder à cette ressource"
},
"warnWhenUnsavedChanges": "Êtes vous sur de vouloir quitter la page? Vous avez des modification non sauvegardées.",
"notifications": {
"success": "Succès",
"error": "Erreur (Code de statut: {{statusCode}})",
"undoable": "Vous avez {{seconds}} secondes à annuler",
"createSuccess": "Création de {{resource}} réussie",
"createError": "Erreur pendant la création de {{resource}} (Code de statut: {{statusCode}})",
"deleteSuccess": "Suppression de {{resource}} réussie",
"deleteError": "Erreur pendant la suppression de {{resource}} (Code de statut: {{statusCode}})",
"editSuccess": "Modification de {{resource}} réussie",
"editError": "Erreur pendant la modification de {{resource}} (Code de statut: {{statusCode}})",
"importProgress": "Importation de: {{processed}}/{{total}}"
},
"loading": "Chargement",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Tableau de bord"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "Sauvegardé",
"error": "Sauvegarde automatique ratée",
"loading": "Sauvegarde...",
"idle": "En attente de modification"
},
"undefined": {
"undefined": "No translation",
"titles": {
"list": "No translation"
}
},
"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",
"lastname": "Nom",
"firstname": "Prénom",
"day_of_birth": "Date de naissance",
"surnames": "Surnoms",
"place_of_birth": "Lieu de naissance",
"resource_title": "Particulier"
},
"corporation": {
"type": "Entreprise",
"title": "Dénomination sociale",
"activity": "Activité",
"employees": "Employés",
"resource_title": "Entreprise"
},
"employee": {
"entity_id": "Identité",
"position": "Poste",
"resource_title": "Employé"
},
"institution": {
"type": "Institution",
"title": "Titre",
"employees": "Employés",
"activity": "Activité",
"resource_title": "Institution"
},
"entity": {
"entity_data": "Informations",
"address": "Adresse",
"resource_title": "Entité",
"resource_plural": "Entités"
},
"provision_template": {
"name": "Nom",
"body": "Corps",
"title": "Titre",
"resource_title": "Template de Clause",
"resource_plural": "Templates de Clauses"
},
"contract_template": {
"name": "Nom",
"title": "Titre",
"parties": "Parties",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Template de Contrat",
"resource_plural": "Templates de Contrats"
},
"party_template": {
"entity_id": "Entité",
"part": "Rôle",
"representative_id": "Représentant",
"resource_title": "Partie"
},
"provision_template_reference": {
"provision_template_id": "Template de clause",
"resource_title": "Template de clause"
},
"dictionary_entry": {
"key": "Variable",
"value": "Valeur",
"resource_title": "Variables"
},
"contract_draft": {
"name": "Nom",
"parties": "Parties",
"title": "Titre",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Brouillon de Contrat",
"resource_plural": "Brouillons de Contrats"
},
"draft_party": {
"part": "Rôle",
"representative_id": "Représentant",
"entity_id": "Entité",
"resource_title": "Partie"
},
"contract_provision_template_reference": {
"type": "Template",
"provision_template_id": "Template de clause",
"resource_title": "Template de clause"
},
"provision_genuine": {
"type": "Personalisée",
"title": "Titre",
"body": "Corps",
"resource_title": "Clause personnalisée"
},
"draft_provision": {
"provision": "Clause",
"resource_title": "Clause"
},
"contract": {
"draft_id": "Brouillon",
"resource_title": "Contrat",
"resource_plural": "Contrats",
"location": "Lieu",
"date": "Date"
}
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,61 +1,102 @@
//https://www.robinwieruch.de/react-router-private-routes/ import { Authenticated, I18nProvider, Refine } from "@refinedev/core";
import { useTranslation } from "react-i18next";
import { useState } from 'react' import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui";
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import {createBrowserRouter, Link, Route, RouterProvider, Routes} from "react-router"; import CssBaseline from "@mui/material/CssBaseline";
import { EntityList } from "./pages/entities/List.tsx"; import GlobalStyles from "@mui/material/GlobalStyles";
import {ProtectedRoute} from "./pages/auth/ProtectedRoute.tsx"; import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
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";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { Login } from "./components/auth/Login";
import { Register } from "./components/auth/Register";
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";
function App() { function App() {
const [user, setUser] = useState<AuthUser | null>(null) const { t, i18n } = useTranslation();
const [count, setCount] = useState(0)
const router = createBrowserRouter([ const i18nProvider: I18nProvider = {
{ translate: (key: string, options?: any) => t(key, options) as string,
path: "/", changeLocale: (lang: string) => i18n.changeLanguage(lang),
element: <div><Link to={"/entities/list"}>To the list</Link></div>, getLocale: () => i18n.language,
}, };
{
path: "/entities/list",
element: <EntityList />,
},
]);
return ( return (
<> <BrowserRouter>
<RouterProvider router={router}> <ColorModeContextProvider>
</RouterProvider> <CssBaseline />
<Routes> <GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<Route index element={ <h1>INDEX</h1> } /> <RefineSnackbarProvider>
<Route element={ <ProtectedRoute user={user} /> }> <Refine
<Route path="toto" element={ <h1>PROTECTED ROUTE</h1>} /> authProvider={authProvider}
</Route> dataProvider={dataProvider}
</Routes> i18nProvider={i18nProvider}
<div> notificationProvider={useNotificationProvider}
<a href="https://vite.dev" target="_blank"> routerProvider={routerBindings}
<img src={viteLogo} className="logo" alt="Vite logo" /> options={{
</a> title: {
<a href="https://react.dev" target="_blank"> text: "Roleplay Contracts",
<img src={reactLogo} className="logo react" alt="React logo" /> icon: <HistoryEduIcon />
</a> },
</div> syncWithLocation: true,
<h1>Vite + React</h1> warnWhenUnsavedChanges: true,
<div className="card"> useNewQueryKeys: true,
<button onClick={() => setCount((count) => count + 1)}> disableTelemetry: true,
count is {count} reactQuery: {
</button> clientConfig: {
<p> defaultOptions: {
Edit <code>src/App.tsx</code> and save to test HMR queries: {
</p> retry: (failureCount, error) => {
</div> // @ts-ignore
<p className="read-the-docs"> const status = error.statusCode ? error.statusCode : error.status
Click on the Vite and React logos to learn more if (status >= 400 && status<= 499) {
</p> return false
</> }
) return failureCount < 4
},
}
}
}
}
}}
>
<I18nTheme>
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" fallback={<Login />}>
<Outlet />
</Authenticated>
)}
>
<Route path="hub/*" element={<HubRoutes />} />
<Route path="firm/*" element={<FirmRoutes />} />
</Route>
<Route path="*" element={<Outlet />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="forgot-password" element={<ForgotPassword />} />
<Route path="update-password" element={<UpdatePassword />} />
</Route>
<Route index element={<><Header /><h1>{t("pages.home.title")}</h1></>} />
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</I18nTheme>
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</BrowserRouter>
);
} }
export default App export default App;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,65 +0,0 @@
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import {RegistryFieldsType, RegistryWidgetsType} from "@rjsf/utils";
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../../providers/jsonschema-provider";
import { useForm } from "@refinedev/core";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
type Props = {
schemaName: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
}
const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const CrudForm: React.FC<Props> = ({schemaName, resource, id}) => {
const { onFinish, query, formLoading } = useForm({
resource: resource,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
});
const record = query?.data?.data;
const [formData, setFormData] = useState(record);
const [schema, setSchema] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaName);
setSchema(resourceSchema);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setLoading(false);
}
};
fetchSchema();
}, []);
return (
<Form
schema={schema}
formData={record}
onChange={(e) => setFormData(e.formData)}
onSubmit={(e) => onFinish(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
/>
)
}

View File

@@ -1,34 +0,0 @@
import { WidgetProps } from "@rjsf/utils";
import { NumericFormat } from "react-number-format";
import TextField from "@mui/material/TextField";
import {InputAdornment} from "@mui/material";
export const BgfcPriceWidget = (props: WidgetProps) => {
const { inputRef, onChange, ...other } = props;
return (
<NumericFormat
{...props}
customInput={TextField}
getInputRef={inputRef}
onValueChange={values => {
onChange({
target: {
name: props.name,
value: values.value
}
});
}}
slotProps={{
input: {
//startAdornment: <InputAdornment position="end">$</InputAdornment>,
endAdornment: <InputAdornment position="start"></InputAdornment>
},
}}
valueIsNumericString={true}
fixedDecimalScale={true}
decimalScale={2}
//defaultValue={0}
/>
);
}

View File

@@ -1,17 +0,0 @@
import TextWidget from "@rjsf/core/lib/components/widgets/TextWidget";
import { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from "@rjsf/utils";
import { ForeignKeyWidget } from "./foreign-key";
import {BgfcPriceWidget} from "./bgfc-price";
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
if (props.schema.hasOwnProperty("foreign_key")) {
return (<ForeignKeyWidget {...props} />);
} else if (props.schema.hasOwnProperty("format") && props.schema.format == "price-bgdc") {
return (<BgfcPriceWidget {...props} />);
} else {
return (<TextWidget {...props} />);
}
}

View File

@@ -1,65 +0,0 @@
import { WidgetProps } from '@rjsf/utils';
import { Autocomplete } from "@mui/material";
import { useState, useEffect } from "react";
import TextField from "@mui/material/TextField";
import {BaseRecord, useList, useOne} from "@refinedev/core";
export const ForeignKeyWidget = (props: WidgetProps) => {
const resource = props.schema.foreign_key.reference.resource
const labelField = props.schema.foreign_key.reference.label
const valueResult = useOne({
resource: resource,
id: props.value != null ? props.value : undefined
});
const empty_option: BaseRecord = {
id: null
}
empty_option[labelField] = "(None)"
const [inputValue, setInputValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
useEffect(() => {
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
return () => clearTimeout(handler);
}, [inputValue]);
const listResult = useList({
resource: resource,
pagination: { current: 1, pageSize: 10 },
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "name", order: "asc" }],
});
const options = listResult.data?.data || [];
if (! props.required) {
options.unshift(empty_option);
}
const isLoading = listResult.isLoading || valueResult.isLoading;
if(! selectedValue && valueResult.data) {
setSelectedValue(valueResult.data?.data)
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue ? newValue : empty_option);
props.onChange(newValue ? newValue.id : null);
return true;
}}
//inputValue={inputValue}
onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
options={options}
getOptionLabel={(option) => option ? option[labelField] : ""}
loading={isLoading}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
};

View File

@@ -1,40 +0,0 @@
import {FieldProps, WidgetProps} from "@rjsf/utils";
import AnyOfField from "@rjsf/core/lib/components/fields/MultiSchemaField";
import {ListSubheader, MenuItem, Select} from "@mui/material";
import {useState} from "react";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
export const UnionEnumWidget = (props: WidgetProps) => {
const {
options,
value,
} = props;
const [selectedValue, setSelectedValue] = useState(null);
if (! selectedValue && value && options.enumOptions) {
for (const opt of options.enumOptions){
if (opt.value == value) {
setSelectedValue(opt);
break;
}
}
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue);
props.onChange(newValue.value);
}}
options={options.enumOptions}
groupBy={(option) => option.type}
getOptionLabel={(option) => option.title}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
}

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,10 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
const DiscordIcon: React.FC<SvgIconProps> = (props) => (
<SvgIcon {...props} viewBox="0 0 24 24">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</SvgIcon>
);
export default DiscordIcon;

View File

@@ -0,0 +1,24 @@
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";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
type SupportedLocales = keyof typeof locales;
export const I18nTheme: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { getLocale } = useTranslation();
const theme = useTheme()
const locale = getLocale() || "en"
const themeWithLocale = createTheme(theme, locales[locale as SupportedLocales])
return (
<ThemeProvider theme={themeWithLocale}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale.slice(0,2)} >
{ children }
</LocalizationProvider>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const ForgotPassword = () => {
return <AuthPage type="forgotPassword" />;
};

View File

@@ -0,0 +1,34 @@
import { useSearchParams, Navigate } from "react-router";
import { useTranslation } from "@refinedev/core";
import { AuthPage } from "@refinedev/mui";
import GoogleIcon from "@mui/icons-material/Google";
import DiscordIcon from "../../components/DiscordIcon";
export const Login = () => {
const { translate } = useTranslation();
const [searchParams] = useSearchParams();
if (searchParams.get("oauth") == "success") {
const redirect_to = localStorage.getItem("redirect_after_login")
if (redirect_to) {
return (<Navigate replace to={ redirect_to } />);
}
}
return (
<AuthPage
type="login"
formProps={{ defaultValues: { email: "test@test.te", password: "test" }, }}
rememberMe={false}
providers={[{
name: "google",
label: translate("pages.login.oauth.google"),
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
},
{
name: "discord",
label: translate("pages.login.oauth.discord"),
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
},
]}
/>
);
};

View File

@@ -0,0 +1,10 @@
import { Button } from "@mui/material";
import { useLogout } from "@refinedev/core";
import { useTranslation } from "@refinedev/core";
export const Logout = () => {
const { translate } = useTranslation();
const { mutate: logout } = useLogout();
return <Button onClick={() => logout()} >{ translate("buttons.logout") }</Button>;
};

View File

@@ -0,0 +1,5 @@
import { AuthPage } from "@refinedev/mui";
export const Register = () => {
return <AuthPage type="register" />;
};

View File

@@ -0,0 +1,6 @@
import { AuthPage } from "@refinedev/mui";
export const UpdatePassword = () => {
return <AuthPage type="updatePassword"/>;
};

View File

@@ -0,0 +1,41 @@
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import React from "react";
import { useTranslation } from "react-i18next";
import { useTranslation as useRefineTranslation } from "@refinedev/core";
const I18nPicker = () => {
const { i18n } = useTranslation();
const { getLocale, changeLocale } = useRefineTranslation();
const currentLocale = getLocale();
return (
<Autocomplete
value={currentLocale}
options={i18n.languages}
disableClearable={true}
renderInput={(params) => {
return <TextField {...params} label={ "Language" } variant="outlined" />
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
component="li"
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
{...optionProps}
>
{ option }
</Box>
);
}}
onChange={(event, value) => {
changeLocale(value);
}}
/>
)
}
export default I18nPicker;

View File

@@ -0,0 +1,142 @@
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
import BusinessIcon from '@mui/icons-material/Business';
import HubIcon from '@mui/icons-material/Hub';
import { Button, Menu, MenuItem } from "@mui/material";
import AppBar from "@mui/material/AppBar";
import Avatar from "@mui/material/Avatar";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import React, { useContext } from "react";
import { Link } from "react-router";
import { useGetIdentity } from "@refinedev/core";
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps, ThemedTitleV2 } from "@refinedev/mui";
import { ColorModeContext } from "../../contexts/color-mode";
import { FirmContext } from "../../contexts/FirmContext";
import { Logout } from "../auth/Logout";
import { IUser } from "../../interfaces";
import MuiLink from "@mui/material/Link";
import I18nPicker from "./I18nPicker";
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const collapsed = false;
const { mode, setMode } = useContext(ColorModeContext);
const { currentFirm } = useContext(FirmContext);
const { data: user } = useGetIdentity<IUser>();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const openUserMenu = Boolean(anchorEl);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}
const handleCloseUserMenu = () => {
setAnchorEl(null);
};
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
<Stack
direction="row"
width="100%"
justifyContent="space-between"
alignItems="center"
>
<HamburgerMenu />
{currentFirm && (
<MuiLink
to={`/firm/${currentFirm.instance}/${currentFirm.firm}`}
component={Link}
underline="none"
sx={{
display: "flex",
alignItems: "center",
gap: "12px",
}}
>
<BusinessIcon height="24px" width="24px" color="primary" />
{!collapsed && (
<Typography
variant="h6"
fontWeight={700}
color="text.primary"
fontSize="inherit"
textOverflow="ellipsis"
overflow="hidden"
>
{currentFirm.instance}&nbsp;/&nbsp;{currentFirm.firm}
</Typography>
)}
</MuiLink>
)}
{!currentFirm && (
<ThemedTitleV2 collapsed={collapsed}/>
)}
{(user?.email) && (
<Link to="/hub"><HubIcon /></Link>
)}
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
>
{(user?.email) && (
<Stack
direction="row"
gap="16px"
alignItems="center"
justifyContent="center"
>
<Button
id="user-menu-button"
aria-controls={openUserMenu ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={openUserMenu ? 'true' : undefined}
onClick={handleOpenUserMenu}>
<Typography
sx={{
display: {
xs: "none",
sm: "inline-block",
},
}}
variant="subtitle2"
>
{user?.email}
</Typography>&nbsp;
<Avatar src={"user?.avatar"} alt={user?.email} />
</Button>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={openUserMenu}
onClose={handleCloseUserMenu}
slotProps={{
list:{ 'aria-labelledby': 'user-menu-button' }
}}
>
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
<MenuItem onClick={setMode}>
<IconButton color="inherit">
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
</MenuItem>
</Menu>
</Stack>
)}
{!user && (
<Link to="/login"><Button>Login</Button></Link>
)}
<I18nPicker />
</Stack>
</Stack>
</Toolbar>
</AppBar>
);
};

View File

@@ -0,0 +1 @@
export { Header } from "./header";

View File

@@ -0,0 +1,52 @@
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>(
{} as FirmContextType
);
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { instance, firm } = useParams<IFirm>();
const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false});
if (instance === undefined || firm === undefined) {
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={value} >
{ children }
</FirmContext.Provider>
);
}

View File

@@ -0,0 +1,59 @@
import { ThemeProvider } from "@mui/material/styles";
import { RefineThemes } from "@refinedev/mui";
import React, {
PropsWithChildren,
createContext,
useEffect,
useState,
} from "react";
type ColorModeContextType = {
mode: string;
setMode: () => void;
};
export const ColorModeContext = createContext<ColorModeContextType>(
{} as ColorModeContextType
);
export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const colorModeFromLocalStorage = localStorage.getItem("colorMode");
const isSystemPreferenceDark = window?.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
const systemPreference = isSystemPreferenceDark ? "dark" : "light";
const [mode, setMode] = useState(
colorModeFromLocalStorage || systemPreference
);
useEffect(() => {
window.localStorage.setItem("colorMode", mode);
}, [mode]);
const setColorMode = () => {
if (mode === "light") {
setMode("dark");
} else {
setMode("light");
}
};
return (
<ColorModeContext.Provider
value={{
setMode: setColorMode,
mode,
}}
>
<ThemeProvider
// you can change the theme colors here. example: mode === "light" ? RefineThemes.Magenta : RefineThemes.MagentaDark
theme={mode === "light" ? RefineThemes.Blue : RefineThemes.BlueDark}
>
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
};

21
gui/rpk-gui/src/i18n.tsx Normal file
View File

@@ -0,0 +1,21 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import detector from "i18next-browser-languagedetector";
i18n
.use(Backend)
.use(detector)
.use(initReactI18next)
.init({
supportedLngs: ["enUS", "frFR"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
},
//saveMissing: true,
ns: ["common"],
defaultNS: "common",
fallbackLng: ["enUS", "frFR"],
});
export default i18n;

View File

@@ -1,68 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

16
gui/rpk-gui/src/index.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./i18n";
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);
root.render(
<React.StrictMode>
<React.Suspense fallback="loading">
<App />
</React.Suspense>
</React.StrictMode>
);

View File

@@ -0,0 +1,16 @@
export type IFirm = {
instance: string,
firm: string
entity?: any
}
type User = {
id: number,
email: string,
firms: [IFirm],
};
export type IAuthUser = User;
export type IUser = User | null;

View File

@@ -0,0 +1,66 @@
import React, { ReactElement, ReactNode } from "react";
import clsx from "clsx";
import { InputLabel, styled } from "@mui/material";
import NotchedOutline from "@mui/material/OutlinedInput/NotchedOutline";
const DivRoot = styled("div")(({ theme }) => ({
position: "relative",
marginTop: "8px",
}));
const DivContentWrapper = styled("div")(({ theme }) => ({
position: "relative",
}));
const DivContent = styled("div")(({ theme }) => ({
paddingTop: "1px",
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
}));
const StyledInputLabel = styled(InputLabel)(({ theme }) => ({
position: "absolute",
left: 0,
top: 0,
}));
const StyledNotchedOutline = styled(NotchedOutline)(({ theme }) => [
{
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.grey["400"]
},
theme.applyStyles('dark', {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.grey["800"]
})
]);
interface Props {
id: string;
label: string;
children: ReactNode;
className?: string;
}
export default function LabelledOutlined({ id, label, children, className }: Props): ReactElement {
const labelRef = React.useRef(null);
return (
<DivRoot className={clsx(className)}>
<StyledInputLabel
ref={labelRef}
htmlFor={id}
variant="outlined"
shrink
>
{label}
</StyledInputLabel>
<DivContentWrapper>
<DivContent id={id}>
{children}
<StyledNotchedOutline notched label={label + "*"} />
</DivContent>
</DivContentWrapper>
</DivRoot>
);
}

View File

@@ -0,0 +1,59 @@
import validator from "@rjsf/validator-ajv8";
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";
import { ParametersContextProvider } from "../contexts/parameters-context";
import CrudArrayField from "./fields/crud-array-field";
type BaseFormProps = {
schema: RJSFSchema,
resourceBasePath: string,
onSubmit?: (data: any) => void,
onChange?: (data: any) => void,
uiSchema?: UiSchema,
formData?: any,
children?: ReactNode
}
export const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget,
};
export const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField,
ArrayField: CrudArrayField
}
const customTemplates = {
ArrayFieldTemplate,
ArrayFieldItemTemplate
}
export const BaseForm: React.FC<BaseFormProps> = (props) => {
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
<ParametersContextProvider>
<Form
schema={schema}
uiSchema={uiSchema === undefined ? {} : uiSchema}
formData={formData}
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
templates={customTemplates}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children}
/>
</ParametersContextProvider>
</ResourceContext.Provider>
)
}

View File

@@ -0,0 +1,73 @@
import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material";
import FilterForm from "../../filter-form/components/filter-form";
import TextField from "@mui/material/TextField";
import { GridExpandMoreIcon } from "@mui/x-data-grid";
import { useResourceFilter } from "../hook";
export type OnChangeValue = {
search: string | null
filters: {[filter: string]: {[op: string]: string}}
}
type CrudFiltersProps = {
resourceName: string
resourcePath: string
onChange: (value: OnChangeValue) => void
}
const CrudFilters = (props: CrudFiltersProps) => {
const { resourceName, resourcePath, onChange } = props
const { hasSearch, filtersSchema, filtersLoading } = useResourceFilter(resourceName, resourcePath)
if (filtersLoading) {
return <CircularProgress />
}
let currentValue = {
search: "",
filters: {}
}
return (
<>
{hasSearch &&
<SearchFilter value="" onChange={(value) => {
currentValue.search = value;
onChange(currentValue);
}}
/>
}
<Accordion>
<AccordionSummary
expandIcon={<>Advanced filters<GridExpandMoreIcon /></>}
/>
<AccordionDetails>
<FilterForm fields={filtersSchema} values={{}} onChange={(value) => {
currentValue.filters = value;
onChange(currentValue);
}}
/>
</AccordionDetails>
</Accordion>
</>
)
}
export default CrudFilters;
type SearchFilter = {
value: string
onChange: (value: any) => void
}
const SearchFilter = (props: SearchFilter) => {
const {value, onChange} = props;
return (
<TextField
label="schemas.search"
fullWidth={true}
onChange={(event) => onChange(event.target.value)}
defaultValue={value}
/>
)
}

View File

@@ -0,0 +1,39 @@
import { ReactNode } from "react";
import { CircularProgress } from "@mui/material";
import { UiSchema } from "@rjsf/utils";
import { BaseForm } from "./base-form";
import { useResourceSchema } from "../hook";
type CrudFormProps = {
schemaName: string,
uiSchema?: UiSchema,
record?: any,
resourceBasePath: string,
onSubmit?: (data: any) => void,
defaultValue?: any,
children?: ReactNode
card?: boolean
}
export const CrudForm: React.FC<CrudFormProps> = (props) => {
const { schemaName, uiSchema, record, resourceBasePath, defaultValue, children, onSubmit=(data: any) => {}, card=false } = props;
const type = record === undefined ? "create" : card ? "card" : "update"
const { schema, schemaLoading } = useResourceSchema(schemaName, type);
if(schemaLoading) {
return <CircularProgress />
}
return (
<BaseForm
schema={schema}
uiSchema={uiSchema}
formData={record || defaultValue}
resourceBasePath={resourceBasePath}
onSubmit={
(data: any) => onSubmit(data)
}
children={children}
/>
)
}

View File

@@ -0,0 +1,56 @@
import { CircularProgress } from "@mui/material";
import { DataGrid, GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
import { UiSchema } from "@rjsf/utils";
import { useResourceColumns } from "../hook";
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, columnLoading } = useResourceColumns<T>(schemaName, columnDefinitions);
if (columnLoading || 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
}
}}
/>
)
}
export default CrudList;

View File

@@ -0,0 +1,146 @@
import React, { useContext } from "react";
import { JSONSchema7Definition } from "json-schema";
import {
FieldProps,
FormContextType,
getTemplate, getUiOptions,
RJSFSchema,
} from "@rjsf/utils";
import ArrayField from "@rjsf/core/lib/components/fields/ArrayField";
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import Typography from "@mui/material/Typography";
import { Box, Paper } from "@mui/material";
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
import { ParametersContext } from "../../contexts/parameters-context";
export type CrudArrayFieldSchema = RJSFSchema & {
props? : any
}
const CrudArrayField = <T = any, S extends CrudArrayFieldSchema = CrudArrayFieldSchema, F extends FormContextType = any> (props: FieldProps<T[], S, F>)=> {
const { schema } = props
let isDictionary = false;
if (schema.props) {
if (schema.props.hasOwnProperty("display")) {
if (schema.props.display == "dictionary") {
isDictionary = true;
}
}
}
if (isDictionary) {
return <Dictionary {...props} />
}
return <ArrayField<T,S,F> {...props}/>
}
export default CrudArrayField;
type DictionaryEntry = {
key: string
value: string
};
const Dictionary = <
T = any,
S extends CrudTextRJSFSchema = CrudTextRJSFSchema,
F extends FormContextType = any
>(props: FieldProps<T[], S, F>)=> {
const { required, formData, onChange, registry, uiSchema, idSchema, schema } = props;
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
const { parameters } = useContext(ParametersContext);
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T[], S, F>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T[], S, F>(
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
let properties = new Set<string>()
for (const field in parameters) {
for (const param of parameters[field]) {
properties.add(param)
}
}
let data: {[key:string]: string} = {}
if (formData !== undefined) {
for (const param of formData) {
// @ts-ignore
data[param.key] = param.value;
}
}
const emptyDict = Object.values(data).length == 0;
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || schema.title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
{ emptyDict && <Typography>No variables found</Typography>}
{ !emptyDict && (
<Form
schema={getFormSchema(Array.from(properties.values()), required || false)}
tagName="div"
formData={data}
validator={validator}
omitExtraData={true}
onChange={(e, id) => {
console.log(e)
let value: T[] = []
for (const prop of properties) {
value.push({
key: prop,
value: e.formData.hasOwnProperty(prop) ? e.formData[prop] : undefined
} as T)
}
onChange(value)
}}
>
&nbsp;
</Form>
)}
</Box>
</Paper>
);
}
function getFormSchema(properties: string[], isRequired: boolean) {
const schema: JSONSchema7Definition = {
type: "object",
properties: {},
};
let required: string[] = []
for (const pname of properties) {
schema.properties![pname] = {
type: "string",
title: pname
}
if (isRequired) {
required.push(pname)
}
}
schema.required = required
return schema
}

View File

@@ -1,11 +1,18 @@
import { ERRORS_KEY, FieldProps, getUiOptions } from "@rjsf/utils"; import {ERRORS_KEY, FieldProps, FormContextType, getUiOptions, RJSFSchema, StrictRJSFSchema} from "@rjsf/utils";
import AnyOfField from "@rjsf/core/lib/components/fields/MultiSchemaField"; import { getDefaultRegistry } from "@rjsf/core";
import { UnionEnumWidget } from "../widgets/union-enum"; import UnionEnumWidget from "../widgets/union-enum";
import get from 'lodash/get'; import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
const UnionEnumField = (props: FieldProps) => { const {
fields: { AnyOfField },
} = getDefaultRegistry();
export default function UnionEnumField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: FieldProps
) {
const { const {
name, name,
disabled = false, disabled = false,
@@ -22,6 +29,8 @@ const UnionEnumField = (props: FieldProps) => {
idSchema, idSchema,
} = props; } = props;
getDefaultRegistry<T,S,F>().widgets
const enumOptions: any[] = [] const enumOptions: any[] = []
if (options.length == 2 && (options[0].type == "null" || options[1].type == "null")) { if (options.length == 2 && (options[0].type == "null" || options[1].type == "null")) {
@@ -89,5 +98,3 @@ const UnionEnumField = (props: FieldProps) => {
/> />
); );
} }
export default UnionEnumField;

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 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

@@ -0,0 +1,25 @@
import React from "react";
import { getDefaultRegistry } from "@rjsf/core";
import { FormContextType, RJSFSchema, WidgetProps } from "@rjsf/utils";
import Typography from "@mui/material/Typography";
import ForeignKeyWidget from "./foreign-key";
import RichtextWidget from "./richtext";
export type CrudTextRJSFSchema = RJSFSchema & { props? : any };
export default function CrudTextWidget<T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
const { schema } = props;
if (schema.hasOwnProperty("foreignKey")) {
return <ForeignKeyWidget {...props} />;
} else if (schema.hasOwnProperty("const")) {
return <Typography >{schema.const as string}</Typography>;
} else if (schema.props?.hasOwnProperty("richtext")) {
return <RichtextWidget {...props} />;
} else {
const { widgets: { TextWidget } } = getDefaultRegistry<T,S,F>();
return <TextWidget {...props} />;
}
}

Some files were not shown because too many files have changed in this diff Show More