48 Commits

Author SHA1 Message Date
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
66 changed files with 5610 additions and 633 deletions

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.10" />
</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>

View File

@@ -5,7 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/api/rpk-api" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/api/.venv" />
</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" />
</component>
</module>

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

@@ -3,10 +3,12 @@ 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

@@ -6,7 +6,7 @@ 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="/draft", tags=["Contract Draft"], )
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", )

View File

@@ -3,9 +3,11 @@ from typing import List, Literal, Optional
from enum import Enum
from uuid import UUID
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey
from firm.core.filter import Filter, FilterSchema
from firm.entity.models import Entity
@@ -24,27 +26,10 @@ class ContractDraftStatus(str, Enum):
class DraftParty(BaseModel):
entity_id: str = Field(
foreignKey={
"reference": {
"resource": "entity",
"schema": "Entity",
}
},
default="",
title="Partie"
)
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Partie")
entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
part: str = Field(title="Rôle")
representative_id: str = Field(
foreignKey={
"reference": {
"resource": "entity",
"schema": "Entity",
}
},
default="",
title="Représentant"
)
representative_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Représentant")
class Config:
title = 'Partie'
@@ -73,14 +58,10 @@ class ProvisionGenuine(BaseModel):
class ContractProvisionTemplateReference(BaseModel):
type: Literal['template'] = ContractProvisionType.template
provision_template_id: str = Field(
foreignKey={
"reference": {
"resource": "template/provision",
"schema": "ProvisionTemplate",
"displayedFields": ['title', 'body']
},
},
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"ProvisionTemplate",
displayed_fields=['title', 'body'],
props={"parametrized": True},
default="",
title="Template de clause"
@@ -172,6 +153,9 @@ class ContractDraft(CrudDocument):
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.

View File

@@ -12,7 +12,7 @@ from weasyprint.text.fonts import FontConfiguration
from pathlib import Path
from firm.core.routes import get_tenant_db_cursor
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
@@ -87,15 +87,15 @@ def retrieve_signature_png(filepath):
preview_router = APIRouter()
@preview_router.get("/draft/{draft_id}", response_class=HTMLResponse, tags=["Contract Draft"])
async def preview_draft(draft_id: str, db=Depends(get_tenant_db_cursor)) -> str:
draft = await build_model(await ContractDraft.get(db, draft_id))
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, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.find_by_signature_id(db, signature_id)
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')
@@ -104,8 +104,8 @@ async def preview_contract_by_signature(signature_id: UUID, db=Depends(get_tenan
@preview_router.get("/{contract_id}", response_class=HTMLResponse, tags=["Contract"])
async def preview_contract(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.get(db, contract_id)
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')
@@ -115,8 +115,8 @@ async def preview_contract(contract_id: str, db=Depends(get_tenant_db_cursor)) -
print_router = APIRouter()
@print_router.get("/pdf/{contract_id}", response_class=FileResponse, tags=["Contract"])
async def create_pdf(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.get(db, contract_id)
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:
@@ -133,7 +133,7 @@ async def create_pdf(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
html.write_pdf(contract_path, stylesheets=[css], font_config=font_config)
await contract.update_status(db, 'printed')
await contract.update_status(reg.db, 'printed')
return FileResponse(
contract_path,
@@ -142,8 +142,8 @@ async def create_pdf(contract_id: str, db=Depends(get_tenant_db_cursor)) -> str:
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse, tags=["Signature"])
async def get_signature_opengraph(signature_id: str, request: Request, db=Depends(get_tenant_db_cursor)) -> str:
contract = await Contract.find_by_signature_id(db, signature_id)
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")

View File

@@ -1,7 +1,8 @@
import uuid
from fastapi import Depends, HTTPException
from firm.core.routes import get_crud_router, get_logged_tenant_db_cursor
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
@@ -16,10 +17,10 @@ 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, db=Depends(get_logged_tenant_db_cursor)) -> ContractRead:
await schema.validate_foreign_key(db)
async def create(schema: ContractCreate, reg=Depends(get_authed_tenant_registry)) -> ContractRead:
await schema.validate_foreign_key(reg.db)
draft = await ContractDraft.get(db, schema.draft_id)
draft = await ContractDraft.get(reg.db, schema.draft_id)
if not draft:
raise HTTPException(status_code=404, detail=f"Contract draft not found!")
@@ -30,7 +31,7 @@ async def create(schema: ContractCreate, db=Depends(get_logged_tenant_db_cursor)
contract_dict = schema.model_dump()
del(contract_dict['draft_id'])
lawyer = await Entity.get(db, db.user.entity_id)
lawyer = await Entity.get(reg.db, reg.user.entity_id)
contract_dict['lawyer'] = lawyer.model_dump()
contract_dict['name'] = draft.name
@@ -38,9 +39,9 @@ async def create(schema: ContractCreate, db=Depends(get_logged_tenant_db_cursor)
parties = []
for p in draft.parties:
parties.append({
'entity': await Entity.get(db, p.entity_id),
'entity': await Entity.get(reg.db, p.entity_id),
'part': p.part,
'representative': await Entity.get(db, p.representative_id) if p.representative_id else None,
'representative': await Entity.get(reg.db, p.representative_id) if p.representative_id else None,
'signature_uuid': str(uuid.uuid4())
})
@@ -49,7 +50,7 @@ async def create(schema: ContractCreate, db=Depends(get_logged_tenant_db_cursor)
provisions = []
for p in draft.provisions:
p = p.provision
provision = await ProvisionTemplate.get(db, p.provision_template_id) if p.type == "template" \
provision = await ProvisionTemplate.get(reg.db, p.provision_template_id) if p.type == "template" \
else p
provisions.append({
@@ -59,11 +60,11 @@ async def create(schema: ContractCreate, db=Depends(get_logged_tenant_db_cursor)
contract_dict['provisions'] = provisions
record = await Contract.create(db, ContractInit(**contract_dict))
await draft.update_status(db, ContractDraftStatus.published)
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, db=Depends(get_logged_tenant_db_cursor)) -> ContractRead:
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

@@ -1,7 +1,8 @@
from beanie import PydanticObjectId
from fastapi import HTTPException, Depends
from firm.core.routes import get_crud_router, get_logged_tenant_db_cursor
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
@@ -13,17 +14,17 @@ del(draft_router.routes[1]) #post route
@draft_router.post("/", response_description="Contract Draft added to the database")
async def create(schema: ContractDraftCreate, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead:
await schema.validate_foreign_key(db)
record = await ContractDraft.create(db, schema)
await record.check_is_ready(db)
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, db=Depends(get_logged_tenant_db_cursor)) -> ContractDraftRead:
record = await ContractDraft.get(db, record_id)
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,
@@ -35,7 +36,7 @@ async def update(record_id: PydanticObjectId, schema: ContractDraftUpdate, db=De
detail="Contract Draft has already been published"
)
record = await ContractDraft.update(db, record, schema)
await record.check_is_ready(db)
record = await ContractDraft.update(reg.db, record, schema)
await record.check_is_ready(reg.db)
return ContractDraftRead.from_model(record)

View File

@@ -4,20 +4,20 @@ import shutil
from uuid import UUID
from firm.contract.models import Contract, Party
from firm.core.routes import get_tenant_db_cursor
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, db=Depends(get_tenant_db_cursor)) -> Party:
contract = await Contract.find_by_signature_id(db, signature_id)
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(...), db=Depends(get_tenant_db_cursor)) -> bool:
contract = await Contract.find_by_signature_id(db, signature_id)
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!")
@@ -31,5 +31,5 @@ async def affix_signature(signature_id: UUID, signature_file: UploadFile = File(
with open(f'media/signatures/{signature_id}.png', "wb") as buffer:
shutil.copyfileobj(signature_file.file, buffer)
await contract.affix_signature(db, signature_index)
await contract.affix_signature(reg.db, signature_index)
return True

View File

@@ -0,0 +1,51 @@
from fastapi import HTTPException, Depends
from hub.auth import get_current_user
from firm.db import get_db_client
from firm.current_firm import CurrentFirmModel
class Registry:
user = None
def __init__(self, db_client, instance, firm):
self.db = db_client[f"tenant_{instance}_{firm}"]
self.instance = instance
self.firm = firm
self.current_firm = CurrentFirmModel.get_current(self.db)
def set_user(self, user):
for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm:
self.user = user
self.db.user = user
return
raise PermissionError
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
registry = Registry(db_client, instance, firm)
if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
return registry
def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry:
try:
registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
return registry
async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
try:
registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
return registry

View File

@@ -9,9 +9,9 @@ from pydantic import BaseModel, Field, computed_field
class CrudDocument(BaseModel):
id: Optional[PydanticObjectId] = Field(default=None)
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
# created_by: str
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
# updated_by: str
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):
@@ -37,8 +37,9 @@ class CrudDocument(BaseModel):
@classmethod
async def create(cls, db, create_schema):
values = cls.model_validate(create_schema.model_dump()).model_dump(mode="json")
result = await cls._get_collection(db).insert_one(values)
model_dict = create_schema.model_dump() | {"created_by": db.user.id, "updated_by": db.user.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)
@@ -56,17 +57,18 @@ class CrudDocument(BaseModel):
@classmethod
async def get(cls, db, model_id):
value = await cls._get_collection(db).find_one({"_id": model_id})
if not value:
document = await cls._get_collection(db).find_one({"_id": model_id})
if not document:
return None
value["id"] = value.pop("_id")
return cls.model_validate(value)
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.user.id}
update_query = {
"$set": {field: value for field, value in update_schema.model_dump(mode="json").items() if field!= "id" }
"$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)
@@ -112,6 +114,20 @@ def RichtextSingleline(*args, **kwargs):
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

@@ -5,44 +5,27 @@ from fastapi_filter import FilterDepends
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.motor import paginate
from hub.auth import get_current_user
from firm.core.depends import get_authed_tenant_registry
from firm.core.models import CrudDocument
from firm.core.schemas import Writer, Reader
from firm.db import get_db_client
#instance: str="westside", firm: str="cht",
def get_tenant_db_cursor(db_client=Depends(get_db_client)):
instance = "westside"
firm = "cht"
return db_client[f"tenant_{instance}_{firm}"]
#instance: str="westside", firm: str="cht",
def get_logged_tenant_db_cursor(db_client=Depends(get_db_client), user=Depends(get_current_user)):
instance = "westside"
firm = "cht"
db_cursor = db_client[f"tenant_{instance}_{firm}"]
db_cursor.user = user
return db_cursor
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), db=Depends(get_logged_tenant_db_cursor)) -> Page[model_read]:
return await paginate(**model.find(db, filters))
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, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
await schema.validate_foreign_key(db)
record = await model.create(db, schema)
return model_read.validate_model(record)
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, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
record = await model.get(db, record_id)
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,
@@ -52,27 +35,27 @@ def get_crud_router(model: CrudDocument, model_create: Writer, model_read: Reade
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, db=Depends(get_logged_tenant_db_cursor)) -> model_read:
record = await model.get(db, record_id)
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(db, record, schema)
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, db=Depends(get_logged_tenant_db_cursor)) -> dict:
record = await model.get(db, record_id)
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(db, record)
await model.delete(reg.db, record)
return {
"message": f"{model_name} deleted successfully"
}

View File

@@ -0,0 +1,72 @@
from typing import Any
from beanie import PydanticObjectId
from pydantic import Field
from firm.core.models import CrudDocument
from firm.core.schemas import Writer, Reader
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
class CurrentFirmModel(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(Reader):
entity: EntityRead
partner: EntityRead
instance: str
firm: str
primary_color: str
secondary_color: str
@classmethod
def from_model_and_entities(cls, model, entity, partner):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner)
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):
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,40 @@
from fastapi import APIRouter, Depends
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
from firm.current_firm import CurrentFirmModel, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.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 CurrentFirmModel.get_current(reg.db)
entity = await Entity.get(reg.db, document.entity_id)
partner = await Partner.get_by_user_id(reg.db, reg.user.id)
partner = await Entity.get(reg.db, partner.entity_id)
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(entity), EntityRead.from_model(partner))
@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)
document = await CurrentFirmModel.create(reg.db, CurrentFirmModel(
instance=reg.instance,
firm=reg.firm,
entity_id=corp.id,
primary_color=schema.primary_color,
secondary_color=schema.secondary_color,
))
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(corp), 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 CurrentFirmModel.get_current(reg.db)
document = await CurrentFirmModel.update(reg.db, document, schema)
return CurrentFirmSchemaRead.from_model(document)

View File

@@ -2,7 +2,7 @@ from datetime import date, datetime
from typing import List, Literal, Optional
from pydantic import Field, BaseModel
from beanie import Indexed
from beanie import Indexed, PydanticObjectId
from firm.core.models import CrudDocument
from firm.core.filter import Filter, FilterSchema
@@ -38,10 +38,11 @@ class Individual(EntityType):
class Employee(BaseModel):
role: Indexed(str) = Field(title='Poste')
entity_id: str = Field(foreignKey={
position: Indexed(str) = Field(title='Poste')
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entity",
"resource": "entities",
"schema": "Entity",
"condition": "entity_data.type=individual"
}

View File

@@ -13,5 +13,11 @@ class EntityCreate(Writer):
class Config:
title = "Création d'un client"
class EntityIndividualCreate(EntityCreate):
entity_data: Individual
class EntityCorporationCreate(EntityCreate):
entity_data: Corporation
class EntityUpdate(EntityCreate):
pass

View File

@@ -5,5 +5,5 @@ from firm.template.routes_provision import router as provision_router
template_router = APIRouter()
template_router.include_router(provision_router, prefix="/provision", )
template_router.include_router(contract_router, prefix="/contract", )
template_router.include_router(provision_router, prefix="/provisions", )
template_router.include_router(contract_router, prefix="/contracts", )

View File

@@ -1,6 +1,7 @@
from typing import List
from html import unescape
from beanie import PydanticObjectId
from pydantic import BaseModel, Field
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry
@@ -8,10 +9,10 @@ from firm.core.filter import Filter, FilterSchema
class PartyTemplate(BaseModel):
entity_id: str = Field(
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entity",
"resource": "entities",
"schema": "Entity",
}
},
@@ -19,10 +20,10 @@ class PartyTemplate(BaseModel):
title="Partie"
)
part: str = Field(title="Rôle")
representative_id: str = Field(
representative_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entity",
"resource": "entities",
"schema": "Entity",
}
},
@@ -61,10 +62,10 @@ class ProvisionTemplate(CrudDocument):
class ProvisionTemplateReference(BaseModel):
provision_template_id: str = Field(
provision_template_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "template/provision",
"resource": "templates/provisions",
"schema": "ProvisionTemplate",
"displayedFields": ['title', 'body']
},

View File

@@ -32,6 +32,19 @@ services:
- "traefik.http.routers.gui.rule=PathPrefix(`/`)"
- "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:
image: traefik:latest
restart: always

View File

@@ -1,2 +1 @@
legacy-peer-deps=true
strict-peer-dependencies=false

File diff suppressed because it is too large Load Diff

View File

@@ -22,10 +22,24 @@
"@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",
"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-hook-form": "^7.30.0",
"react-i18next": "^15.5.1",
"react-router": "^7.0.2"
},
"devDependencies": {

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,273 @@
{
"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": {
"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"
},
"provision_template": {
"name": "Name",
"title": "Title",
"body": "Body",
"resource_title": "Provision Template"
},
"contract_template": {
"name": "Name",
"title": "Title",
"provisions": "Provisions",
"parties": "Parties",
"variables": "Variables",
"resource_title": "Contract Template"
},
"party_template": {
"entity_id": "Party Template",
"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"
},
"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",
"draft_id": "Draft"
}
}
}

View File

@@ -0,0 +1,273 @@
{
"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": {
"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": "Titre",
"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é"
},
"provision_template": {
"name": "Nom",
"body": "Corps",
"title": "Titre",
"resource_title": "Template de Clause"
},
"contract_template": {
"name": "Nom",
"title": "Titre",
"parties": "Parties",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Template de Contrat"
},
"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"
},
"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",
"location": "Lieu",
"date": "Date"
}
}
}

View File

@@ -1,21 +1,20 @@
import { Authenticated, Refine } from "@refinedev/core";
import { Authenticated, I18nProvider, Refine } from "@refinedev/core";
import { useTranslation } from "react-i18next";
import {
RefineSnackbarProvider, RefineThemes,
useNotificationProvider,
} from "@refinedev/mui";
import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { ThemeProvider } from "@mui/material/styles";
import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
import routerBindings, {
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router";
import { authProvider } from "./providers/auth-provider";
import { dataProvider } from "./providers/data-provider";
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";
@@ -25,11 +24,20 @@ import { UpdatePassword } from "./components/auth/UpdatePassword";
import { Header } from "./components";
import { HubRoutes } from "./pages/hub";
import { FirmRoutes } from "./pages/firm";
import rpcTheme from "./theme";
function App() {
const { t, i18n } = useTranslation();
const i18nProvider: I18nProvider = {
translate: (key: string, options?: any) => t(key, options) as string,
changeLocale: (lang: string) => i18n.changeLanguage(lang),
getLocale: () => i18n.language,
};
return (
<BrowserRouter>
<ThemeProvider theme={RefineThemes.Blue}>
<ThemeProvider theme={rpcTheme}>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
@@ -37,9 +45,14 @@ function App() {
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
title: {
text: "Roleplay Contracts",
icon: <HistoryEduIcon />
},
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
@@ -50,7 +63,7 @@ function App() {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
if (error.status >= 400 && error.status <= 499) {
if (error.statusCode >= 400 && error.statusCode <= 499) {
return false
}
return failureCount < 4
@@ -64,7 +77,7 @@ function App() {
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" redirectOnFail="/auth/login" fallback={<CatchAllNavigate to="/auth/login"/>}>
<Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}>
<Outlet />
</Authenticated>
)}
@@ -72,13 +85,13 @@ function App() {
<Route path="hub/*" element={<HubRoutes />} />
<Route path="firm/*" element={<FirmRoutes />} />
</Route>
<Route path="auth/*" element={<><Header /><Outlet /></>}>
<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>HOME&nbsp;</h1></>} />
<Route index element={<><Header /><h1>{t("pages.home.title")}</h1></>} />
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />

View File

@@ -1,10 +1,11 @@
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";
import { useSearchParams, Navigate } from "react-router";
export const Login = () => {
const { translate } = useTranslation();
const [searchParams] = useSearchParams();
if (searchParams.get("oauth") == "success") {
const redirect_to = localStorage.getItem("redirect_after_login")
@@ -16,14 +17,15 @@ export const Login = () => {
<AuthPage
type="login"
formProps={{ defaultValues: { email: "test@test.te", password: "test" }, }}
rememberMe={false}
providers={[{
name: "google",
label: "Sign in with Google",
label: translate("pages.login.oauth.google"),
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
},
{
name: "discord",
label: "Sign in with Discord",
label: translate("pages.login.oauth.discord"),
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
},
]}

View File

@@ -1,8 +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()} >Logout</Button>;
return <Button onClick={() => logout()} >{ translate("buttons.logout") }</Button>;
};

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

@@ -1,5 +1,6 @@
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";
@@ -11,15 +12,18 @@ import Typography from "@mui/material/Typography";
import React, { useContext } from "react";
import { Link } from "react-router";
import { useGetIdentity } from "@refinedev/core";
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui";
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);
@@ -45,12 +49,38 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
>
<HamburgerMenu />
{currentFirm && (
<Link to={`/firm/${currentFirm.instance}/${currentFirm.firm}`} ><Typography variant="h4" >{currentFirm.instance}&nbsp;/&nbsp;{currentFirm.firm}</Typography></Link>
<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 && (
<Link to="/" ><Typography variant="h4">Roleplay&nbsp;Contracts</Typography></Link>
<ThemedTitleV2 collapsed={collapsed}/>
)}
{(user?.email) && (
<Link to="/hub"><HubIcon /></Link>
)}
<Stack
direction="row"
justifyContent="flex-end"
@@ -92,13 +122,8 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
}}
>
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
<MenuItem onClick={handleCloseUserMenu}>
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
<MenuItem onClick={setMode}>
<IconButton color="inherit">
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
</MenuItem>
@@ -106,9 +131,9 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
</Stack>
)}
{!user && (
<Link to="/auth/login"><Button>Login</Button></Link>
<Link to="/login"><Button>Login</Button></Link>
)}
<I18nPicker />
</Stack>
</Stack>
</Toolbar>

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: ["EN", "FR"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
},
//saveMissing: true,
ns: ["common"],
defaultNS: "common",
fallbackLng: ["EN", "FR"],
});
export default i18n;

View File

@@ -2,12 +2,15 @@ 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,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,46 @@
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 { ResourceContext } from "../contexts/ResourceContext";
import { ReactNode } from "react";
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
}
export const BaseForm: React.FC<BaseFormProps> = (props) => {
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
<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}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children}
/>
</ResourceContext.Provider>
)
}

View File

@@ -1,67 +1,64 @@
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 { ReactNode, useEffect, useState } from "react";
import { CircularProgress } from "@mui/material";
import { useForm } from "@refinedev/core";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
import { UiSchema } from "@rjsf/utils";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { BaseForm } from "./base-form";
type Props = {
type CrudFormProps = {
schemaName: string,
uiSchema?: UiSchema,
resourceBasePath?: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
onSuccess?: (data: any) => void
onSuccess?: (data: any) => void,
defaultValue?: any,
children?: ReactNode
}
const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
export const CrudForm: React.FC<CrudFormProps> = (props) => {
const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess, defaultValue, children } = props;
const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const CrudForm: React.FC<Props> = ({ schemaName, resource, id, onSuccess }) => {
const { onFinish, query, formLoading } = useForm({
resource: resource,
resource: resourceBasePath == "" ? resource : `${resourceBasePath}/${resource}`,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } },
});
const record = query?.data?.data;
const [formData, setFormData] = useState(record);
const [schema, setSchema] = useState({});
const [loading, setLoading] = useState(true);
const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaName);
const schemaFullName = id === undefined ? `${schemaName}Create` : `${schemaName}Update`;
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaFullName);
setSchema(resourceSchema);
setLoading(false);
setSchemaLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setLoading(false);
setSchemaLoading(false);
}
};
fetchSchema();
}, []);
if(formLoading || schemaLoading) {
return <CircularProgress />
}
const record = query?.data?.data || defaultValue;
return (
<Form
<BaseForm
schema={schema}
uiSchema={uiSchema}
formData={record}
onChange={(e) => setFormData(e.formData)}
onSubmit={(e) => onFinish(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
resourceBasePath={resourceBasePath}
onSubmit={
(data: any) => onFinish(data)
}
children={children}
/>
)
}

View File

@@ -1,15 +1,25 @@
import {FormContextType, getTemplate, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils";
import React from "react";
import { getDefaultRegistry } from "@rjsf/core";
import { FormContextType, getTemplate, RJSFSchema, WidgetProps } from "@rjsf/utils";
import Typography from "@mui/material/Typography";
import ForeignKeyWidget from "./foreign-key";
import RichtextWidget from "./richtext";
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
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>
) {
if (props.schema.hasOwnProperty("foreign_key")) {
return (<ForeignKeyWidget {...props} />);
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 { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options);
return <BaseInputTemplate {...props} />;
const { widgets: { TextWidget } } = getDefaultRegistry<T,S,F>();
return <TextWidget {...props} />;
}
}

View File

@@ -1,79 +1,239 @@
import {FormContextType, RJSFSchema, StrictRJSFSchema, 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";
import { FormContextType, RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils';
import {
Autocomplete, Button, CircularProgress, Container, Grid2, InputAdornment, Modal, TextField, Box, DialogContent
} from "@mui/material";
import ClearIcon from '@mui/icons-material/Clear';
import EditIcon from '@mui/icons-material/Edit';
import NoteAddIcon from '@mui/icons-material/NoteAdd';
import React, { useState, useEffect, useContext, Fragment } from "react";
import { useList, useOne } from "@refinedev/core";
import { ResourceContext } from "../../contexts/ResourceContext";
import { CrudForm } from "../crud-form";
type ForeignKeySchema = RJSFSchema & {
foreign_key?: {
reference: {
export type ForeignKeyReference = {
resource: string,
label: string
label?: string,
displayedFields?: [string],
schema: string
}
export type ForeignKeySchema = RJSFSchema & {
foreignKey?: {
reference: ForeignKeyReference
}
}
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
if (props.schema.foreign_key === undefined) {
if (props.schema.foreignKey === undefined) {
return;
}
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 { value: originalValue, onChange } = props;
const [currentValue, setCurrentValue] = useState(originalValue)
const empty_option: BaseRecord = {
id: undefined
if (currentValue) {
return <ChosenValue {...props} onClear={() => {setCurrentValue(null); onChange(null)}}/>
}
empty_option[labelField] = "(None)"
return <RealAutocomplete {...props} onChange={(value) => {setCurrentValue(value); onChange(value)}}/>
};
const [inputValue, setInputValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
if (props.schema.foreignKey === undefined) {
return;
}
const { onChange, label } = props
const [openFormModal, setOpenFormModal] = useState(false);
const [searchString, setSearchString] = useState<string>("");
const [debouncedInputValue, setDebouncedInputValue] = useState<string>();
useEffect(() => {
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
const handler = setTimeout(() => setDebouncedInputValue(searchString), 300); // Adjust debounce delay as needed
return () => clearTimeout(handler);
}, [inputValue]);
}, [searchString]);
const listResult = useList({
resource: resource,
pagination: { current: 1, pageSize: 10 },
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "name", order: "asc" }],
const { resource, schema, label: labelField = "label" } = props.schema.foreignKey.reference
const { basePath } = useContext(ResourceContext)
const { data, isLoading } = useList({
resource: `${basePath}/${resource}`,
pagination: { current: 1, pageSize: 10, mode: "server" },
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "label", order: "asc" }],
});
const options = listResult.data?.data || [];
if (! props.required) {
options.unshift(empty_option);
return (
<>
<Autocomplete
onChange={(event, value) => {
onChange(value ? value.id : null);
return true;
}}
onInputChange={(event, value) => {
setSearchString(value)
}}
options={data ? data.data : []}
getOptionLabel={(option) => option ? option[labelField] : ""}
loading={isLoading}
forcePopupIcon={false}
renderInput={(params) => (
<TextField
{...params}
label={ label }
variant="outlined"
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<Button variant="outlined" onClick={() => setOpenFormModal(true)} color="success" >
<NoteAddIcon />
</Button>
),
},
}}
/>
)}
/>
<Modal
open={openFormModal}
onClose={() => setOpenFormModal(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainer
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
uiSchema={{}}
onSuccess={(data: any) => {
setOpenFormModal(false)
onChange(data.data.id);
}}
/>
</DialogContent>
</Modal>
</>
);
}
const isLoading = listResult.isLoading || valueResult.isLoading;
if(! selectedValue && valueResult.data) {
setSelectedValue(valueResult.data?.data)
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F> & { onClear: () => void }
) => {
const { onClear, value } = props;
const [openFormModal, setOpenFormModal] = React.useState(false);
if (props.schema.foreignKey === undefined) {
return;
}
const { resource, schema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
const { basePath } = useContext(ResourceContext)
const { data, isLoading } = useOne({
resource: `${basePath}/${resource}`,
id: value
});
if (isLoading || data === undefined) {
return <CircularProgress />
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue ? newValue : empty_option);
props.onChange(newValue ? newValue.id : null);
return true;
<>
<TextField label={ props.label } variant="outlined" disabled={true} value={data.data[labelField]}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<Button variant="outlined" onClick={() => setOpenFormModal(true)} color="primary" >
<EditIcon />
</Button>
<Button variant="outlined" onClick={onClear} color="error" >
<ClearIcon />
</Button>
</InputAdornment>),
},
}}
//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" />
)}
/>
);
{ displayedFields && <Preview id={value} basePath={basePath} resource={resource} displayedFields={displayedFields}/>}
<Modal
open={openFormModal}
onClose={() => setOpenFormModal(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainer
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
uiSchema={{}}
id={value}
onSuccess={() => setOpenFormModal(false)}
/>
</DialogContent>
</Modal>
</>
)
}
const modalStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
pt: 2,
px: 4,
pb: 3,
};
type FormContainerProps = {
schemaName: string,
resourceBasePath: string,
resource: string,
uiSchema?: UiSchema,
id?: string,
onSuccess: (data: any) => void
}
const FormContainer = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={(data) => onSuccess(data)} />
</Box>
)
}
const Preview = (props: {id: string, resource: string, basePath: string, displayedFields: [string]}) => {
const { basePath, resource, id, displayedFields } = props
const { data, isLoading } = useOne({
resource: `${basePath}/${resource}`,
id
});
if (isLoading || data === undefined) {
return <CircularProgress />
}
return (
<Grid2 container spacing={2}>
{displayedFields.map((field: string, index: number) => {
return (
<Fragment key={index}>
<Grid2 size={2}><Container>{field}</Container></Grid2>
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: data.data[field] }} ></Container></Grid2>
</Fragment>
)
})}
</Grid2>
);
}

View File

@@ -0,0 +1,119 @@
import { Extension } from "@tiptap/core";
type IndentOptions = {
/**
* @default ["paragraph", "heading"]
*/
types: string[];
/**
* Amount of margin to increase and decrease the indent
*
* @default 40
*/
margin: number;
};
declare module "@tiptap/core" {
interface Commands<ReturnType> {
indent: {
indent: () => ReturnType;
outdent: () => ReturnType;
};
}
}
export default Extension.create<IndentOptions>({
name: "indent",
defaultOptions: {
types: ["paragraph", "heading"],
margin: 40
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
indent: {
default: 0,
renderHTML: (attrs) => ({
style: `margin-left: ${(attrs.indent || 0) * this.options.margin}px`
}),
parseHTML: (attrs) => parseInt(attrs.style.marginLeft) / this.options.margin || 0
}
}
}
];
},
addCommands() {
return {
indent:
() =>
({ editor, chain, commands }) => {
// Check for a list
if (
editor.isActive("listItem") ||
editor.isActive("bulletList") ||
editor.isActive("orderedList")
) {
return chain().sinkListItem("listItem").run();
}
return this.options.types
.map((type) => {
const attrs = editor.getAttributes(type).indent;
const indent = (attrs || 0) + 1;
return commands.updateAttributes(type, { indent });
})
.every(Boolean);
},
outdent:
() =>
({ editor, chain, commands }) => {
// Check for a list
if (
editor.isActive("listItem") ||
editor.isActive("bulletList") ||
editor.isActive("orderedList")
) {
return chain().liftListItem("listItem").run();
}
const result = this.options.types
.filter((type) => {
const attrs = editor.getAttributes(type).indent;
return attrs > 0;
})
.map((type) => {
const attrs = editor.getAttributes(type).indent;
const indent = (attrs || 0) - 1;
return commands.updateAttributes(type, { indent });
});
return result.every(Boolean) && result.length > 0;
}
};
},
// addKeyboardShortcuts() {
// return {
// Tab: ({ editor }) => {
// return editor.commands.indent();
// },
// "Shift-Tab": ({ editor }) => {
// return editor.commands.outdent();
// },
// Backspace: ({ editor }) => {
// const { selection } = editor.state;
//
// // Make sure we are at the start of the node
// if (selection.$anchor.parentOffset > 0 || selection.from !== selection.to) {
// return false;
// }
//
// return editor.commands.outdent();
// }
// };
// }
});

View File

@@ -0,0 +1,19 @@
import FormatIndentIncrease from "@mui/icons-material/FormatIndentIncrease";
import { MenuButton, MenuButtonProps, useRichTextEditorContext } from "mui-tiptap";
export type MenuButtonIndentProps = Partial<MenuButtonProps>;
export default function MenuButtonIndent(props: MenuButtonIndentProps) {
const editor = useRichTextEditorContext();
return (
<MenuButton
tooltipLabel="Indent"
tooltipShortcutKeys={["Tab"]}
IconComponent={FormatIndentIncrease}
disabled={!editor?.isEditable || (!editor.can().indent())}
onClick={() => editor?.chain().focus().indent().run()}
{...props}
/>
);
}

View File

@@ -0,0 +1,18 @@
import FormatIndentDecrease from "@mui/icons-material/FormatIndentDecrease";
import { MenuButton, MenuButtonProps, useRichTextEditorContext } from "mui-tiptap";
export type MenuButtonUnindentProps = Partial<MenuButtonProps>;
export default function MenuButtonUnindent(props: MenuButtonUnindentProps) {
const editor = useRichTextEditorContext();
return (
<MenuButton
tooltipLabel="Unindent"
tooltipShortcutKeys={["Shift", "Tab"]}
IconComponent={FormatIndentDecrease}
disabled={!editor?.isEditable || !editor.can().outdent()}
onClick={() => editor?.chain().focus().outdent().run()}
{...props}
/>
);
}

View File

@@ -0,0 +1,145 @@
import { FormContextType, WidgetProps } from "@rjsf/utils";
import React from "react";
import { CrudTextRJSFSchema } from "../crud-text-widget";
import { useEditor, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline"
import TextAlign from "@tiptap/extension-text-align"
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import {
MenuButtonAddTable, MenuButtonAlignCenter, MenuButtonAlignJustify, MenuButtonAlignLeft, MenuButtonAlignRight,
MenuButtonBold, MenuButtonBulletedList, MenuButtonItalic, MenuButtonOrderedList, MenuButtonRedo, MenuButtonUnderline,
MenuButtonUndo, MenuControlsContainer, MenuDivider, RichTextEditorProvider, RichTextField, TableBubbleMenu,
TableImproved,
} from "mui-tiptap";
import { UseEditorOptions } from "@tiptap/react/src/useEditor";
import LabelledOutlined from "../../../../LabelledOutlined";
import MenuButtonUnindent from "./MenuButtonUnindent";
import MenuButtonIndent from "./MenuButtonIndent";
import IndentExtension from "./IndentExtension"
import { Container, Paper, styled } from "@mui/material";
import Stack from "@mui/material/Stack";
const LeftContainer = styled(Container)(({ theme }) => [{
width: "2cm",
borderLeft: "8px ridge black",
borderRight: "1px dashed grey",
padding: "0px !important",
margin: "0px !important"
},
]);
const TextContainer = styled(Container)(({ theme }) => [{
maxWidth: "580px",
padding: "0px !important",
margin: "0px !important"
},
]);
const RightContainer = styled(Container)(({ theme }) => [{
width: "2cm",
borderRight: "8px groove black",
borderLeft: "1px dashed grey",
padding: "0px !important",
margin: "0px !important"
},
]);
const StyledLabelledOutlined = styled(LabelledOutlined)(({ theme }) => [{
padding: "1px !important",
},
]);
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
const { schema, value, onChange, label, id } = props;
const isMultiline = schema.props.multiline === true;
let editorOptions: UseEditorOptions;
if (isMultiline) {
editorOptions = {
extensions: [StarterKit, Underline, TextAlign.configure({types: ['paragraph', "table"]}), TableImproved.configure({resizable: true}), TableRow, TableHeader, TableCell, IndentExtension],
onUpdate: ({ editor }) => {
onChange(editor.getHTML())
}
}
} else {
editorOptions = {
extensions: [StarterKit, Underline,],
onUpdate: ({ editor }) => {
let text = editor.getText();
if (text.includes("\n")) {
text = text.replace("\n", " ");
editor.commands.setContent(text);
}
onChange(text);
}
}
}
editorOptions.content = value
const editor = useEditor(editorOptions)
return (
<StyledLabelledOutlined label={label} id={id}>
<Stack direction="row" spacing={0} sx={{justifyContent: "center", alignItems: "stretch"}}>
<LeftContainer>&nbsp;</LeftContainer>
<TextContainer>
<RichTextEditorProvider editor={editor}>
<TableBubbleMenu />
<RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>
</RichTextEditorProvider>
</TextContainer>
<RightContainer>&nbsp;</RightContainer>
</Stack>
</StyledLabelledOutlined>
)
}
export default RichtextWidget
const singlelineButtons = (
<>
<MenuButtonUndo tabIndex={-1} />
<MenuButtonRedo tabIndex={-1} />
<MenuDivider />
<MenuButtonBold tabIndex={-1} />
<MenuButtonItalic tabIndex={-1} />
<MenuButtonUnderline tabIndex={-1} />
</>
);
const multilineButtons = (
<>
<MenuButtonUndo tabIndex={-1} />
<MenuButtonRedo tabIndex={-1} />
<MenuDivider />
<MenuButtonBold tabIndex={-1} />
<MenuButtonItalic tabIndex={-1} />
<MenuButtonUnderline tabIndex={-1} />
<MenuDivider />
<MenuButtonAlignLeft tabIndex={-1} />
<MenuButtonAlignCenter tabIndex={-1} />
<MenuButtonAlignRight tabIndex={-1} />
<MenuButtonAlignJustify tabIndex={-1} />
<MenuDivider />
<MenuButtonUnindent tabIndex={-1} />
<MenuButtonIndent tabIndex={-1} />
<MenuButtonBulletedList tabIndex={-1} />
<MenuButtonOrderedList tabIndex={-1} />
<MenuDivider />
<MenuButtonAddTable tabIndex={-1} />
</>
);

View File

@@ -0,0 +1,9 @@
import React, { createContext, PropsWithChildren } from 'react';
type ResourceContextType = {
basePath: string,
}
export const ResourceContext = createContext<ResourceContextType>(
{} as ResourceContextType
);

View File

@@ -1,5 +1,5 @@
import { JSONSchema7Definition } from "json-schema";
import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n'
const API_URL = "/api/v1";
@@ -19,9 +19,14 @@ const getJsonschema = async (): Promise<RJSFSchema> => {
return rawSchema;
}
function convertCamelToSnake(str: string): string {
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
}
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
let resource;
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, ""));
resource = structuredClone(rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
@@ -45,6 +50,13 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
} else if (is_array(prop) && is_reference(prop.items)) {
resolveReference(rawSchemas, resource, prop.items);
}
if (prop.hasOwnProperty("title")) {
prop.title = i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title);
}
}
if (resource.hasOwnProperty("title")) {
resource.title = i18n.t(`schemas.${shortResourceName}.resource_title`, resource.title);
}
return resource;
@@ -161,7 +173,6 @@ function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string)
return has_descendant(rawSchemas, resource, path);
}
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
&& path_exists(
rawSchemas,

View File

@@ -0,0 +1,34 @@
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Contract = {
id: string,
label: string
}
export const ContractRoutes = () => {
return (
<Routes>
<Route index element={ <ListContract /> } />
<Route path="/edit/:record_id" element={ <EditContract /> } />
<Route path="/create" element={ <CreateContract /> } />
</Routes>
);
}
const ListContract = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Contract> resource={`contracts`} columns={columns} />
}
const EditContract = () => {
return <Edit<Contract> resource={`contracts`} schemaName={"Contract"} />
}
const CreateContract = () => {
return <New<Contract> resource={`contracts`} schemaName={"Contract"} />
}

View File

@@ -0,0 +1,114 @@
import { Route, Routes } from "react-router";
import { CircularProgress } from "@mui/material";
import React, { useContext, useState } from "react";
import { useOne } from "@refinedev/core";
import { BaseForm } from "../../lib/crud/components/base-form";
import { ForeignKeyReference, ForeignKeySchema } from "../../lib/crud/components/widgets/foreign-key";
import { FirmContext } from "../../contexts/FirmContext";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Draft = {
id: string,
label: string
}
export const DraftRoutes = () => {
return (
<Routes>
<Route index element={ <ListDraft /> } />
<Route path="/edit/:record_id" element={ <EditDraft /> } />
<Route path="/create" element={ <CreateDraft /> } />
</Routes>
);
}
const ListDraft = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Draft> resource={`contracts/drafts`} columns={columns} />
}
const EditDraft = () => {
return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
}
type ForeignKeySubSchema = ForeignKeySchema & {
properties: { [key: string]: { foreignKey: { reference: ForeignKeyReference } } }
}
const CreateDraft = () => {
const [chosenDraft, setChosenDraft] = useState<string|null>(null)
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const templateFieldSchema: ForeignKeySubSchema = {
type: "object",
properties: {
template_id: {
type: "string",
title: "Find a template",
foreignKey: {
reference: {
resource: "templates/contracts",
schema: "ContractTemplate"
}
}
}
},
};
const templateForm = (
<BaseForm
schema={templateFieldSchema}
formData={{template_id: chosenDraft}}
resourceBasePath={resourceBasePath}
onChange={(data) => {
const { template_id } = data;
setChosenDraft(template_id);
}}
>
&nbsp;
</BaseForm>
)
if (chosenDraft !== null) {
return (
<>
{templateForm}
<CreateDraftFromTemplate template_id={chosenDraft}/>
</>
)
}
return (
<>
{templateForm}
<New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
</>
)
}
const CreateDraftFromTemplate = (props: { template_id: string }) => {
const { template_id } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const resource = "templates/contracts"
const { data, isLoading } = useOne({
resource: `${resourceBasePath}/${resource}`,
id: template_id
});
if (isLoading || data === undefined) {
return <CircularProgress />
}
let template = { ...data.data };
template.provisions = data.data.provisions.map((item: any) => {
return { provision: {type: "template", provision_template_id: item.provision_template_id} }
})
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} defaultValue={ template }/>
}

View File

@@ -0,0 +1,37 @@
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Entity = {
id: string,
label: string,
entity_data: { type: string },
}
export const EntityRoutes = () => {
return (
<Routes>
<Route index element={ <ListEntity /> } />
<Route path="/edit/:record_id" element={ <EditEntity /> } />
<Route path="/create" element={ <CreateEntity /> } />
</Routes>
);
}
const ListEntity = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
{ field: "entity_data", headerName: "Type", flex: 1, valueFormatter: ({ type }: {type: string}) => type }
];
return <List<Entity> resource={`entities`} columns={columns} />
}
const EditEntity = () => {
return <Edit<Entity> resource={`entities`} schemaName={"Entity"} />
}
const CreateEntity = () => {
return <New<Entity> resource={`entities`} schemaName={"Entity"} />
}

View File

@@ -0,0 +1,35 @@
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Provision = {
id: string,
label: string,
}
export const ProvisionRoutes = () => {
return (
<Routes>
<Route index element={ <ListProvision /> } />
<Route path="/edit/:record_id" element={ <EditProvision /> } />
<Route path="/create" element={ <CreateProvision /> } />
</Routes>
);
}
const ListProvision = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Provision> resource={`templates/provisions`} columns={columns} />
}
const EditProvision = () => {
return <Edit<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} />
}
const CreateProvision = () => {
return <New<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} />
}

View File

@@ -0,0 +1,34 @@
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
type Template = {
id: string,
label: string,
}
export const TemplateRoutes = () => {
return (
<Routes>
<Route index element={ <ListTemplate /> } />
<Route path="/edit/:record_id" element={ <EditTemplate /> } />
<Route path="/create" element={ <CreateTemplate /> } />
</Routes>
);
}
const ListTemplate = () => {
const columns = [
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Template> resource={`templates/contracts`} columns={columns} />
}
const EditTemplate = () => {
return <Edit<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} />
}
const CreateTemplate = () => {
return <New<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} />
}

View File

@@ -0,0 +1,48 @@
import { UiSchema } from "@rjsf/utils";
import { useParams } from "react-router";
import { useContext } from "react";
import { Button } from "@mui/material";
import DeleteIcon from '@mui/icons-material/Delete';
import SaveIcon from '@mui/icons-material/Save';
import { FirmContext } from "../../../contexts/FirmContext";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import Stack from "@mui/material/Stack";
import { DeleteButton } from "@refinedev/mui";
type EditProps = {
resource: string,
schemaName: string,
uiSchema?: UiSchema,
}
const Edit = <T,>(props: EditProps) => {
const { schemaName, resource, uiSchema } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { record_id } = useParams();
return (
<>
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
id={record_id}
>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "space-between",
alignItems: "center",
}}>
<Button type='submit' variant="contained" size="large"><SaveIcon />Save</Button>
<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>
</Stack>
</CrudForm>
</>
)
}
export default Edit;

View File

@@ -0,0 +1,46 @@
import { UiSchema } from "@rjsf/utils";
import { List as RefineList, useDataGrid } from "@refinedev/mui";
import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid";
import { Link, useNavigate } from "react-router"
import React, { useContext } from "react";
import { Button } from "@mui/material";
import { FirmContext } from "../../../contexts/FirmContext";
type ListProps<T extends GridValidRowModel> = {
resource: string,
columns: GridColDef<T>[],
schemaName?: string,
uiSchema?: UiSchema,
}
const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
const { resource, columns } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { dataGridProps } = useDataGrid<T>({resource: `${resourceBasePath}/${resource}`});
const navigate = useNavigate();
const cols = React.useMemo<GridColDef<T>[]>(
() => columns,
[],
);
const handleRowClick = (params: any, event: any) => {
navigate(`edit/${params.id}`)
}
return (
<RefineList>
<Link to={"create"} >
<Button>Create</Button>
</Link>
<DataGrid
{...dataGridProps}
columns={cols}
onRowClick={handleRowClick} />
</RefineList>
)
}
export default List;

View File

@@ -0,0 +1,29 @@
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { UiSchema } from "@rjsf/utils";
import { useContext } from "react";
import { FirmContext } from "../../../contexts/FirmContext";
type NewProps = {
resource: string,
schemaName: string,
uiSchema?: UiSchema,
defaultValue?: any
}
const New = <T,>(props: NewProps) => {
const { schemaName, resource, uiSchema, defaultValue } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
return (
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
defaultValue={defaultValue}
/>
)
}
export default New;

View File

@@ -1,28 +1,80 @@
import {Route, Routes} from "react-router";
import { Route, Routes, Link } from "react-router";
import React, { useContext } from "react";
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
import { Header } from "../../components";
import { useOne } from "@refinedev/core";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { IFirm } from "../../interfaces";
import { EntityRoutes } from "./EntityRoutes";
import { ContractRoutes } from "./ContractRoutes";
import { DraftRoutes } from "./DraftRoutes";
import { TemplateRoutes } from "./TemplateRoutes";
import { ProvisionRoutes } from "./ProvisionRoutes";
export const FirmRoutes = () => {
return (
<>
<Routes>
<Route path="/:instance/:firm/*" element={
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
<Route path="/entities/*" element={ <EntityRoutes /> } />
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
<Route path="/templates/*" element={ <TemplateRoutes /> } />
<Route path="/drafts/*" element={ <DraftRoutes /> } />
<Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes>
</FirmContextProvider>
} />
</Routes>
</>
);
}
const FirmHome = () => {
const { currentFirm } = useContext(FirmContext);
const { data: firm, isError, error, isLoading } = useOne({resource: 'firm', id: `${currentFirm.instance}/${currentFirm.firm}/`, errorNotification: false})
if (isLoading) {
return <h1>Loading...</h1>
}
if (isError && error?.statusCode == 405) {
return <FirmInitForm currentFirm={currentFirm} />
}
return (
<>
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
<ul>
<li><Link to="entities">Entitées</Link></li>
<li><Link to="provisions">Templates de Clauses</Link></li>
<li><Link to="templates">Templates de Contrats</Link></li>
<li><Link to="drafts">Brouillons</Link></li>
<li><Link to="contracts">Contrats</Link></li>
</ul>
</>
);
}
type FirmInitFormPros = {
currentFirm: IFirm
}
const FirmInitForm = (props: FirmInitFormPros) => {
const { currentFirm } = props;
return (
<>
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
<CrudForm
schemaName={"CurrentFirmSchemaCreate"}
resource={`firm/${currentFirm.instance}/${currentFirm.firm}/`}
uiSchema={{
corporation: {entity_data: {employees: {"ui:style": {"display": "none"}}}},
}}
/>
</>
)
}

View File

@@ -12,7 +12,7 @@ export const CreateFirm = () => {
return (
<CrudForm
schemaName={"FirmCreate"}
resource={"firms"}
resource={"hub/users/firms/"}
onSuccess={() => { refreshUser() }}
/>
)

View File

@@ -22,8 +22,8 @@ export const HubRoutes = () => {
const HubHome = () => {
const { data: user } = useGetIdentity<IAuthUser>();
const { data: list } = useList<IFirm>({resource: "hub/users/firms/", pagination: { mode: "off" }}, )
if (user === undefined || list === undefined) {
const { data: list } = useList<IFirm>({resource: "hub/users/firms", pagination: { mode: "off" }}, )
if (user === undefined || user === null || list === undefined) {
return <p>Loading</p>;
}
const ownedFirms = list.data;

View File

@@ -9,7 +9,7 @@ const DISCORD_SCOPES = { "scopes": "identify email" }
const DEFAULT_LOGIN_REDIRECT = "/hub"
export const authProvider: AuthProvider = {
const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to");
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param
@@ -72,7 +72,7 @@ export const authProvider: AuthProvider = {
if (get_user() == null) {
return {
authenticated: false,
redirectTo: "/auth/login",
redirectTo: "/login",
logout: true
}
}
@@ -154,7 +154,7 @@ export const authProvider: AuthProvider = {
if (error?.status === 401) {
forget_user();
return {
redirectTo: "/auth/login",
redirectTo: "/login",
logout: true,
error: { message: "Authentication required" },
} as OnErrorResponse;
@@ -192,10 +192,12 @@ export function empty_user() {
function findGetParameter(parameterName: string) {
let result = null, tmp = [];
location.search.substr(1).split("&")
location.search.substring(1).split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
export default authProvider;

View File

@@ -1,11 +1,23 @@
import type { DataProvider } from "@refinedev/core";
import type { DataProvider, HttpError } from "@refinedev/core";
const API_URL = "/api/v1";
export const dataProvider: DataProvider = {
const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
const response = id !== "" ? await fetch(`${API_URL}/${resource}/${id}`) : await fetch(`${API_URL}/${resource}`);
if (response.status < 200 || response.status > 299) throw response;
if (id === "") {
return { data: undefined };
}
const response = await fetch(`${API_URL}/${resource}/${id}`);
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -22,8 +34,16 @@ export const dataProvider: DataProvider = {
},
});
if (response.status < 200 || response.status > 299) throw response;
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
return { data };
@@ -44,14 +64,23 @@ export const dataProvider: DataProvider = {
if (filters && filters.length > 0) {
filters.forEach((filter) => {
if ("field" in filter && filter.value && filter.operator === "contains") {
params.append(filter.field + "__like", "%" + filter.value + "%");
params.append(filter.field + "__ilike", filter.value);
}
});
}
const response = await fetch(`${API_URL}/${resource}?${params.toString()}`);
const response = await fetch(`${API_URL}/${resource}/?${params.toString()}`);
if (response.status < 200 || response.status > 299) throw response;
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -67,7 +96,7 @@ export const dataProvider: DataProvider = {
};
},
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/${resource}`, {
const response = await fetch(`${API_URL}/${resource}/`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
@@ -75,7 +104,16 @@ export const dataProvider: DataProvider = {
},
});
if (response.status < 200 || response.status > 299) throw response;
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -86,7 +124,16 @@ export const dataProvider: DataProvider = {
method: "DELETE",
});
if (response.status < 200 || response.status > 299) throw response;
if (response.status < 200 || response.status > 299) {
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -102,3 +149,5 @@ export const dataProvider: DataProvider = {
// updateMany: () => { /* ... */ },
// custom: () => { /* ... */ },
};
export default dataProvider;

View File

@@ -0,0 +1,5 @@
import { createTheme } from "@mui/material/styles";
const rpcTheme = createTheme({});
export default rpcTheme

11
i18n/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:lts-alpine
WORKDIR /app
COPY app/package*.json ./
RUN npm install
COPY app/tsconfig*.json ./
COPY app/src ./src
EXPOSE 8100
CMD [ "npm", "--watch", "start" ]

26
i18n/app/.gitignore vendored Normal file
View File

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

1164
i18n/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
i18n/app/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "i18n Helper",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.15.3",
"express": "^5.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"body-parser": "^2.2.0"
}
}

View File

49
i18n/app/src/index.ts Normal file
View File

@@ -0,0 +1,49 @@
import express, { Request, Response } from 'express';
import bodyParser from "body-parser";
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
const app = express();
app.use(bodyParser.json());
const port = process.env.PORT || 8100;
app.post('/locales/add/:lng/:ns', (req: Request, res: Response) => {
const keyPath = Object.keys(req.body)[0];
const basePath = "public/locales";
const lgPath = `${basePath}/${req.params.lng}`;
if (!existsSync(lgPath)) {
mkdirSync(lgPath);
}
const filePath = `${lgPath}/common.json`;
let missingTrans;
try {
missingTrans = JSON.parse(readFileSync(filePath, 'utf8'));
} catch(err) {
missingTrans = JSON.parse("{}");
}
let current = missingTrans
const splitPath = keyPath.split(".");
for (let i=0; i < splitPath.length; i++) {
const key = splitPath[i];
if (! current.hasOwnProperty(key)) {
if (i + 1 == splitPath.length) {
current[key] = "No translation";
} else {
current[key] = JSON.parse("{}");
}
}
current = current[key];
}
writeFileSync(filePath, JSON.stringify(missingTrans, null, 2))
res.send("OK");
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

13
i18n/app/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}