107 Commits

Author SHA1 Message Date
8d4d64076a Correcting list navigation 2023-03-26 21:00:38 +02:00
f9867b3e2b Changing the contract file name to the contract label. Updating label content with a dash 2023-03-24 13:42:07 +01:00
b89d043880 Updating favicon 2023-03-21 21:33:22 +01:00
0ecbb99423 Square border radii 2023-03-20 16:02:27 +01:00
61bd5589ff Adding static meta to index.html 2023-03-20 16:00:46 +01:00
72065c0d0d Adding static meta data on signatures 2023-03-20 14:30:43 +01:00
cd8bb58dfb Revert "Adding a label to signature read entities"
This reverts commit 191a3d0018.
2023-03-20 14:05:25 +01:00
9ab1571067 Revert "Adding metadata for social networks on signature page"
This reverts commit 2948e9b961.
2023-03-20 14:04:27 +01:00
2948e9b961 Adding metadata for social networks on signature page 2023-03-19 13:55:57 +01:00
191a3d0018 Adding a label to signature read entities 2023-03-19 13:55:13 +01:00
8885969a07 Removing nickname from individual's labels 2023-03-19 13:54:38 +01:00
a997e54891 Adding an autogenerated label to contracts 2023-03-19 13:53:07 +01:00
0b12f8718a Decorating the sidenav 2023-03-18 18:29:37 +01:00
d5d2f31178 Correcting signature translation bug 2023-03-18 17:35:06 +01:00
1bd2774cdd Correcting redirection error after contract creation 2023-03-18 17:27:35 +01:00
e5375edda8 Changing website title 2023-03-18 17:20:08 +01:00
ae306bb89e Correcting overselecting menue 2023-03-18 17:13:36 +01:00
e877c36a3d Moving contracts/drafts to contract-drafts 2023-03-18 17:07:29 +01:00
7f639fc5a0 Correcting overselecting menue 2023-03-18 17:05:58 +01:00
7fbba953cd separating logoutbutton from rest of menu 2023-03-18 16:28:18 +01:00
4e52999b5d Progression translations 2023-03-18 16:21:55 +01:00
392d66b03e Correcting bug caused by not deep enough clones 2023-03-18 16:12:54 +01:00
dc6616bba6 Use address bar to remember list params 2023-03-17 17:45:50 +01:00
f3f0ddc004 Implementing enus is jsonschema utilities 2023-03-17 17:39:20 +01:00
0b9d5d42cb Increasing flashmessage display duration to human-readable length 2023-03-17 15:23:58 +01:00
bb0ecb5c13 Sending an error message on contract failure 2023-03-17 15:23:21 +01:00
5a5f1b3519 Correcting foreignKey not updated on update 2023-03-17 15:15:53 +01:00
2adecb99d2 Removing htmltags in list values 2023-03-17 15:06:31 +01:00
3db7c62b09 Filtering out htmlentities in rich text field 2023-03-17 14:36:40 +01:00
78c4ee119a Improving table list display 2023-03-17 14:35:55 +01:00
46ac3295e3 Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 15:36:53 +01:00
2349f4c804 Adding a makefile 2023-03-16 15:36:36 +01:00
334150bc0f Preparing dockers for prod 2023-03-16 15:03:06 +01:00
ewandor
da19ef652e Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-16 03:47:47 +01:00
ewandor
015fc00f6f using root when locating assets in contract print 2023-03-16 03:47:29 +01:00
e22c197d3a Involving gittea image registry 2023-03-16 02:55:55 +01:00
becab58c73 Adding templates to migration scripts 2023-03-16 02:24:35 +01:00
70a863f6e1 Correcting nginx configuration for prod 2023-03-16 02:24:21 +01:00
2b55d206e2 Naming containers and freezing mongo version to 4.4.19 2023-03-16 02:23:54 +01:00
f35182233b Adding a paste filter on richtext editor 2023-03-15 15:45:11 +01:00
43474c960f Merge remote-tracking branch 'origin/fix/default-date-contract-creation' 2023-03-15 15:19:21 +01:00
ewandor
a16a122713 Correcting default date format on contract creation 2023-03-15 15:17:43 +01:00
b3566b39b8 Removing a typo 2023-03-15 15:16:09 +01:00
7bebc05e08 fulltext search use each word separately 2023-03-15 15:15:47 +01:00
6b49b688ac Removing unused imports 2023-03-14 19:09:49 +01:00
7dbe4a1716 Changing db password 2023-03-14 19:06:13 +01:00
701ac8e1dc Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-14 19:00:18 +01:00
2ba34a675d Production env for the front 2023-03-14 18:59:56 +01:00
08cb2772ea Updating default value of contract create form 2023-03-14 15:59:23 +01:00
86bcb87427 Updating date input type to allow empty dates 2023-03-14 15:59:01 +01:00
4be6591e81 Hidding birthplace when empty on contract 2023-03-14 15:55:18 +01:00
b4f81431b9 Making entity birthday optionalble 2023-03-14 15:07:00 +01:00
b1d0e115f4 Dynamic asset url depending on request host 2023-03-14 14:36:02 +01:00
8aac5376df Adding translation for Cratract draft creation form 2023-03-14 14:13:59 +01:00
918ad94861 Correcting preview link 2023-03-14 14:13:18 +01:00
b598e7a147 Infinite lifetime access_token 2023-03-13 19:03:28 +01:00
54082700e8 Merge branch 'master' of git.dorfsvald.net:ewandor/cht-lawfirm 2023-03-13 15:42:17 +01:00
4a4fca0a40 Downgrading mongo to run on leto 2023-03-13 15:42:00 +01:00
45116fbb8c Adding a title to customer create schema 2023-03-13 02:20:48 +01:00
2b784850c7 Empty login form after login 2023-03-12 21:01:53 +01:00
427ea64f8d Translations for most of contract & templates 2023-03-12 19:12:35 +01:00
868eea79bf Finishing translation of entity module 2023-03-12 13:43:49 +01:00
3390acbc82 Display column title name in crudlist 2023-03-12 13:23:49 +01:00
8319fa9fac Translating Entity resource 2023-03-12 13:07:10 +01:00
ad19a50346 Adding filter display and management in Crud front 2023-03-11 22:50:14 +01:00
f2ddc4303e Adding In filter to back 2023-03-11 22:48:27 +01:00
598a962aba Adding full text search to contract and drafts 2023-03-11 22:47:54 +01:00
1753271ac0 Preparing crud components for filters arrival 2023-03-11 22:46:54 +01:00
2cb5af904e Return textual error for permissions 2023-03-11 18:13:47 +01:00
1d3918db87 Better feedback on draft publication 2023-03-10 17:27:14 +01:00
a52435d443 Improving common list displays 2023-03-09 17:44:30 +01:00
1e3855ced5 Bumping created_at & updated_at fields to the end of schemas 2023-03-09 16:39:05 +01:00
d313f5a3d8 Adding a label to signature link 2023-03-09 16:38:28 +01:00
0f1c910919 First round of internationlization 2023-03-09 15:53:18 +01:00
0011b62a5d Login and logout menu display madolities 2023-03-08 23:48:52 +01:00
5605ee9497 Contract Signing and contract printing 2023-03-08 21:59:10 +01:00
eaa79c3541 Addign folders for signature & contract storage 2023-03-08 21:53:52 +01:00
bf536fe8f7 Adding simple permission to routes 2023-03-08 15:08:45 +01:00
ac3268e6c8 Endding entity_id to user model 2023-03-08 15:07:39 +01:00
9da0063812 Removing unused routes in main 2023-03-08 15:06:01 +01:00
b7880dc304 Adding loging logout logic 2023-03-07 14:33:29 +01:00
374f90b3ec Converting app compoenent ts to 4 spaces 2023-03-07 14:26:05 +01:00
3ecfb7fa3b Correcting authentification path in openai 2023-03-07 14:12:14 +01:00
f94fdcd141 Improving nickname display 2023-03-07 14:09:41 +01:00
0d484209f6 Correcting bug caused y Entity name collision 2023-03-07 14:09:01 +01:00
52ea9dfb63 Dynamic lawyer 2023-03-06 21:25:11 +01:00
4b00ef12aa Open preview in new tab 2023-03-06 21:22:42 +01:00
34cb9bece4 Removing unused templates 2023-03-06 21:22:19 +01:00
d43164c93d Creating a normal preview for contract drafts 2023-03-06 21:06:51 +01:00
8bb3d0f44e updating contract preview 2023-03-06 20:22:41 +01:00
9035824248 feading right data to signature form 2023-03-06 20:21:57 +01:00
ab0111c1f5 Correcting contract forms display 2023-03-06 20:21:12 +01:00
02e2685c50 Adding psecial link type 2023-03-06 20:19:40 +01:00
62bbe706ca Making array readonly-able 2023-03-06 17:59:21 +01:00
576b5970a5 Adding contract creation 2023-03-06 17:05:49 +01:00
d8c8ebdc48 broadening hidden field usage 2023-03-06 16:49:32 +01:00
ac34dd1663 Adding a newline at end of file 2023-03-06 16:48:14 +01:00
ac981d18fc Signature Widget 2023-03-05 14:43:20 +01:00
da634c59ee Moving an gular cli installation up to not redo it every time we add a library 2023-03-05 14:29:59 +01:00
4d31497b8b contract signature ready 2023-03-03 20:49:56 +01:00
5322756e5a contract draft specific path 2023-03-03 20:48:42 +01:00
9621f90e25 removing useless python class 2023-03-03 20:46:42 +01:00
5773179c0d correcting sidenav 2023-03-03 20:45:27 +01:00
35e532449b Merge branch 'feature/pdf-print' 2023-02-19 16:36:05 +01:00
6d6dc2d82b Adding contract location/date and party reprensetative 2023-02-17 19:43:56 +01:00
61b8e3fe21 Adding logo as title 2023-02-17 19:42:06 +01:00
ab6490622a Only extract parameters of non n 2023-02-17 19:41:25 +01:00
98 changed files with 4239 additions and 822 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
__pycache__/ __pycache__/
back/app/fixtures/ back/app/fixtures/
back/media/
front/app-back/ front/app-back/
front/app-back2/ front/app-back2/

12
Makefile Normal file
View File

@@ -0,0 +1,12 @@
publish:
git checkout $(TAG)
docker build -f back/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-back-prod back
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:$(TAG)
docker build -f front/Dockerfile.prod -t git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod front
docker tag git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
docker push git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:$(TAG)
git switch -

View File

@@ -5,13 +5,10 @@ RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cf
WORKDIR /code WORKDIR /code
# copy both 'package.json' and 'package-lock.json' (if available)
COPY ./requirements.txt /code/requirements.txt COPY ./requirements.txt /code/requirements.txt
# install project dependencies
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY ./app /code/app COPY ./app /code/app
EXPOSE 8000 EXPOSE 8000

15
back/Dockerfile.prod Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.10
RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,9 +1,105 @@
from fastapi import APIRouter import uuid
from fastapi import Depends, HTTPException, File, UploadFile
import shutil
from ..core.routes import get_crud_router
from .routes_draft import draft_router from .routes_draft import draft_router
from .print import print_router from .print import print_router
contract_router = APIRouter() from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value
from .schemas import ContractCreate, ContractRead, ContractUpdate
contract_router.include_router(draft_router, prefix="/draft", tags=["draft"], ) from ..entity.models import Entity
contract_router.include_router(print_router, prefix="/print", tags=["print"], ) from ..template.models import ProvisionTemplate
from ..user.manager import get_current_user, get_current_superuser
contract_router = get_crud_router(Contract, ContractCreate, ContractRead, ContractUpdate)
del(contract_router.routes[0])
del(contract_router.routes[2])
del(contract_router.routes[2])
contract_router.include_router(draft_router, prefix="/draft", )
contract_router.include_router(print_router, prefix="/print", )
@contract_router.post("/", response_description="Contract Successfully created")
async def create(item: ContractCreate, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key()
draft = await ContractDraft.get(item.draft_id)
for v in draft.variables:
if not v.key or not v.value:
raise HTTPException(status_code=400, detail="Variable {} is invalid".format(v))
contract_dict = item.dict()
del(contract_dict['draft_id'])
contract_dict['lawyer'] = await Entity.get(user.entity_id)
contract_dict['name'] = draft.name
contract_dict['title'] = draft.title
parties = []
for p in draft.parties:
parties.append({
'entity': await Entity.get(p.entity_id),
'part': p.part,
'representative': await Entity.get(p.representative_id) if p.representative_id else None,
'signature_uuid': str(uuid.uuid4())
})
contract_dict['parties'] = parties
provisions = []
for p in draft.provisions:
p = p.provision
provision = await ProvisionTemplate.get(p.provision_template_id) if p.type == 'template' \
else p
provisions.append({
'title': replace_variables_in_value(draft.variables, provision.title),
'body': replace_variables_in_value(draft.variables, provision.body)
})
contract_dict['provisions'] = provisions
o = await Contract(**contract_dict).create()
await draft.update({"$set": {"status": ContractDraftStatus.published}})
return {"message": "Contract Successfully created", "id": o.id}
@contract_router.put("/{id}", response_description="")
async def update(id: str, contract_form: ContractUpdate, user=Depends(get_current_superuser)) -> ContractRead:
raise HTTPException(status_code=400, detail="No modification on contract")
@contract_router.get("/signature/{signature_id}", response_description="")
async def get_signature(signature_id: str) -> Party:
contract = await Contract.find_by_signature_id(signature_id)
signature = contract.get_signature(signature_id)
return signature
@contract_router.post("/signature/{signature_id}", response_description="")
async def affix_signature(signature_id: str, signature_file: UploadFile = File(...)) -> bool:
contract = await Contract.find_by_signature_id(signature_id)
signature_index = contract.get_signature_index(signature_id)
signature = contract.parties[signature_index]
if signature.signature_affixed:
raise HTTPException(status_code=400, detail="Signature already affixed")
with open(f'media/signatures/{signature_id}.png', "wb") as buffer:
shutil.copyfileobj(signature_file.file, buffer)
update_query = {"$set": {
f'parties.{signature_index}.signature_affixed': True
}}
signature.signature_affixed = True
if contract.is_signed():
update_query["$set"]['status'] = 'signed'
await contract.update(update_query)
return True

View File

@@ -2,33 +2,38 @@ import datetime
from typing import List, Literal from typing import List, Literal
from enum import Enum from enum import Enum
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, validator
from beanie.operators import ElemMatch
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
from ..entity.models import Entity
class ContractStatus(str, Enum): class ContractStatus(str, Enum):
new = 'new' published = 'published'
signed = 'signed' signed = 'signed'
in_effect = 'in_effect' printed = 'printed'
executed = 'executed' executed = 'executed'
class ContractDraftStatus(str, Enum): class ContractDraftStatus(str, Enum):
draft = 'draft' in_progress = 'in_progress'
created = 'created' ready = 'ready'
published = 'published'
class Party(BaseModel): class DraftParty(BaseModel):
entity_id: str = Field( entity_id: str = Field(
foreignKey={ foreignKey={
"reference": { "reference": {
"resource": "entity", "resource": "entity",
"schema": "Entity", "schema": "Entity",
} }
} },
default="",
title="Partie"
) )
part: str part: str = Field(title="Rôle")
representative_id: str = Field( representative_id: str = Field(
foreignKey={ foreignKey={
"reference": { "reference": {
@@ -36,14 +41,30 @@ class Party(BaseModel):
"schema": "Entity", "schema": "Entity",
} }
}, },
default="" default="",
title="Représentant"
) )
class Config:
title = 'Partie'
class Party(BaseModel):
entity: Entity
part: str
representative: Entity = None
signature_uuid: str
signature_affixed: bool = False
signature_png: str = None
class ProvisionGenuine(BaseModel): class ProvisionGenuine(BaseModel):
type: Literal['genuine'] = 'genuine' type: Literal['genuine'] = 'genuine'
title: str = RichtextSingleline(props={"parametrized": True}) title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
body: str = RichtextMultiline(props={"parametrized": True}) body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
class Config:
title = 'Clause personalisée'
class ContractProvisionTemplateReference(BaseModel): class ContractProvisionTemplateReference(BaseModel):
@@ -56,23 +77,157 @@ class ContractProvisionTemplateReference(BaseModel):
"displayedFields": ['title', 'body'] "displayedFields": ['title', 'body']
}, },
}, },
props={"parametrized": True} props={"parametrized": True},
default="",
title="Template de clause"
) )
class Config:
title = 'Template de clause'
class DraftProvision(BaseModel): class DraftProvision(BaseModel):
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type') provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Config:
title = 'Clause'
class Provision(BaseModel):
title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline(title="Corps")
class ContractDraft(CrudDocument): class ContractDraft(CrudDocument):
name: str """
title: str Brouillon de contrat à remplir
parties: List[Party] """
provisions: List[DraftProvision]
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[DraftParty] = Field(title="Parties")
provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field( variables: List[DictionaryEntry] = Field(
default=[], default=[],
format="dictionary", format="dictionary",
title='Variables'
) )
status: ContractDraftStatus = Field(default=ContractDraftStatus.draft) status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
location: str = "" todo: List[str] = Field(default=[], title="Reste à faire")
date: datetime.date = datetime.date(1970, 1, 1)
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class Config:
title = 'Brouillon de contrat'
async def check_is_ready(self):
if self.status == ContractDraftStatus.published:
return
self.todo = []
if len(self.parties) < 2:
self.todo.append('Contract must have at least two parties')
if len(self.provisions) < 1:
self.todo.append('Contract must have at least one provision')
for p in self.parties:
if not p.entity_id:
self.todo.append('All parties must have an associated entity`')
for p in self.provisions:
if p.provision.type == "genuine" and not (p.provision.title and p.provision.body):
self.todo.append('Empty genuine provision')
elif p.provision.type == "template" and not p.provision.provision_template_id:
self.todo.append('Empty template provision')
for v in self.variables:
if not (v.key and v.value):
self.todo.append('Empty variable')
if self.todo:
self.status = ContractDraftStatus.in_progress
else:
self.status = ContractDraftStatus.ready
await self.update({"$set": {
"status": self.status,
"todo": self.todo
}})
class Contract(CrudDocument):
"""
Contrat publié. Les contrats ne peuvent pas être modifiés.
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
"""
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[Party] = Field(title="Parties")
provisions: List[Provision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
lawyer: Entity = Field(title="Avocat en charge")
location: str = Field(title="Lieu")
date: datetime.date = Field(title="Date")
label: str = None
@validator("label", always=True)
def generate_label(cls, v, values, **kwargs):
if not v:
contract_label = values['title']
for p in values['parties']:
contract_label = contract_label + f" - {p.entity.label}"
contract_label = contract_label + f" - {values['date'].strftime('%m/%d/%Y')}"
return contract_label
return v
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
@classmethod
def find_by_signature_id(cls, signature_id: str):
crit = ElemMatch(cls.parties, {"signature_uuid": signature_id})
return cls.find_one(crit)
def get_signature(self, signature_id: str):
for p in self.parties:
if p.signature_uuid == signature_id:
return p
def get_signature_index(self, signature_id: str):
for i, p in enumerate(self.parties):
if p.signature_uuid == signature_id:
return i
def is_signed(self):
for p in self.parties:
if not p.signature_affixed:
return False
return True
def replace_variables_in_value(variables, value: str):
for v in variables:
if v.value:
value = value.replace('%{}%'.format(v.key), v.value)
return value

View File

@@ -1,4 +1,8 @@
from fastapi import APIRouter import datetime
import os
import base64
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -9,7 +13,7 @@ from pathlib import Path
from app.entity.models import Entity from app.entity.models import Entity
from app.template.models import ProvisionTemplate from app.template.models import ProvisionTemplate
from ..schemas import ContractDraft from ..models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
async def build_model(model): async def build_model(model):
@@ -24,19 +28,28 @@ async def build_model(model):
parties.append(party) parties.append(party)
model.parties = parties model.parties = parties
provisions = [] provisions = []
for p in model.provisions: for p in model.provisions:
if p.provision.type == "template": if p.provision.type == "template":
provisions.append(await ProvisionTemplate.get(p.provision.provision_template_id)) provision = await ProvisionTemplate.get(p.provision.provision_template_id)
else: else:
provisions.append(p.provision) provision = p.provision
provision.title = replace_variables_in_value(model.variables, provision.title)
provision.body = replace_variables_in_value(model.variables, provision.body)
provisions.append(provision)
model.provisions = provisions model.provisions = provisions
model.location = "Toulouse" model = model.dict()
model.date = "01/01/1970" model['location'] = "Los Santos, SA"
model['date'] = datetime.date(1970, 1, 1)
model['lawyer'] = {'entity_data': {
"firstname": "prénom avocat",
"lastname": "nom avocat",
}}
return model return model
@@ -48,49 +61,96 @@ print_router = APIRouter()
templates = Jinja2Templates(directory=str(BASE_PATH / "templates")) templates = Jinja2Templates(directory=str(BASE_PATH / "templates"))
async def render_print(host, draft, lawyer): async def render_print(root_url, contract):
template = templates.get_template("print.html") template = templates.get_template("print.html")
return template.render({ return template.render({
"draft": draft, "contract": contract,
"lawyer": lawyer, "root_url": root_url
"static_host": host
}) })
async def render_css(host, draft): async def render_css(root_url, contract):
template = templates.get_template("styles.css") template = templates.get_template("styles.css")
return template.render({ return template.render({
"draft": draft, "contract": contract,
"static_host": host "root_url": root_url
}) })
@print_router.get("/", response_class=HTMLResponse) @print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse)
async def create() -> str: async def preview_draft(draft_id: str, request: Request) -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4")) draft = await build_model(await ContractDraft.get(draft_id))
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
return await render_print('localhost', draft, lawyer) return await render_print('', draft)
@print_router.get("/pdf", response_class=FileResponse) @print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
async def create_pdf() -> str: async def preview_contract_by_signature(signature_id: str, request: Request) -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4")) contract = await Contract.find_by_signature_id(signature_id)
lawyer = { for p in contract.parties:
"firstname": "Nathaniel", if p.signature_affixed:
"lastname": "Toshi", p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
}
font_config = FontConfiguration() return await render_print('', contract)
html = HTML(string=await render_print('nginx', draft, lawyer))
css = CSS(string=await render_css('nginx', draft), font_config=font_config)
html.write_pdf('out.pdf', stylesheets=[css], font_config=font_config)
@print_router.get("/preview/{contract_id}", response_class=HTMLResponse)
async def preview_contract(contract_id: str, request: Request) -> str:
contract = await Contract.get(contract_id)
for p in contract.parties:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
return await render_print('', contract)
@print_router.get("/pdf/{contract_id}", response_class=FileResponse)
async def create_pdf(contract_id: str) -> str:
contract = await Contract.get(contract_id)
contract_path = "media/contracts/{}.pdf".format(contract_id)
if not os.path.isfile(contract_path):
if contract.status != ContractStatus.signed:
raise HTTPException(status_code=400, detail="Contract is not in a printable state")
for p in contract.parties:
signature_path = f'media/signatures/{p.signature_uuid}.png'
p.signature_png = retrieve_signature_png(signature_path)
# os.remove(signature_path)
font_config = FontConfiguration()
html = HTML(string=await render_print('http://nginx', contract))
css = CSS(string=await render_css('http://nginx', contract), font_config=font_config)
html.write_pdf(contract_path, stylesheets=[css], font_config=font_config)
update_query = {"$set": {
'status': 'printed'
}}
await contract.update(update_query)
return FileResponse( return FileResponse(
"out.pdf", contract_path,
media_type="application/pdf", media_type="application/pdf",
filename=draft.name) filename=contract.label)
def retrieve_signature_png(filepath):
with open(filepath, "rb") as f:
b_content = f.read()
base64_utf8_str = base64.b64encode(b_content).decode('utf-8')
ext = filepath.split('.')[-1]
return f'data:image/{ext};base64,{base64_utf8_str}'
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
async def get_signature_opengraph(signature_id: str, request: Request) -> str:
contract = await Contract.find_by_signature_id(signature_id)
signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html")
signatory = signature.representative.label if signature.representative else signature.entity.label
return template.render({
"signatory": signatory,
"title": contract.label,
"origin_url": f"{request.url.scheme}://{request.url.hostname}"
})

View File

@@ -1,152 +0,0 @@
from weasyprint import HTML, CSS
class PdfGenerator:
"""
Generate a PDF out of a rendered template, with the possibility to integrate nicely
a header and a footer if provided.
Notes:
------
- When Weasyprint renders an html into a PDF, it goes though several intermediate steps.
Here, in this class, we deal mostly with a box representation: 1 `Document` have 1 `Page`
or more, each `Page` 1 `Box` or more. Each box can contain other box. Hence the recursive
method `get_element` for example.
For more, see:
https://weasyprint.readthedocs.io/en/stable/hacking.html#dive-into-the-source
https://weasyprint.readthedocs.io/en/stable/hacking.html#formatting-structure
- Warning: the logic of this class relies heavily on the internal Weasyprint API. This
snippet was written at the time of the release 47, it might break in the future.
- This generator draws its inspiration and, also a bit of its implementation, from this
discussion in the library github issues: https://github.com/Kozea/WeasyPrint/issues/92
"""
OVERLAY_LAYOUT = '@page {size: A4 portrait; margin: 0;}'
def __init__(self, main_html, header_html=None, footer_html=None,
base_url=None, side_margin=2, extra_vertical_margin=30):
"""
Parameters
----------
main_html: str
An HTML file (most of the time a template rendered into a string) which represents
the core of the PDF to generate.
header_html: str
An optional header html.
footer_html: str
An optional footer html.
base_url: str
An absolute url to the page which serves as a reference to Weasyprint to fetch assets,
required to get our media.
side_margin: int, interpreted in cm, by default 2cm
The margin to apply on the core of the rendered PDF (i.e. main_html).
extra_vertical_margin: int, interpreted in pixel, by default 30 pixels
An extra margin to apply between the main content and header and the footer.
The goal is to avoid having the content of `main_html` touching the header or the
footer.
"""
self.main_html = main_html
self.header_html = header_html
self.footer_html = footer_html
self.base_url = base_url
self.side_margin = side_margin
self.extra_vertical_margin = extra_vertical_margin
def _compute_overlay_element(self, element: str):
"""
Parameters
----------
element: str
Either 'header' or 'footer'
Returns
-------
element_body: BlockBox
A Weasyprint pre-rendered representation of an html element
element_height: float
The height of this element, which will be then translated in a html height
"""
html = HTML(
string=getattr(self, f'{element}_html'),
base_url=self.base_url,
)
element_doc = html.render(stylesheets=[CSS(string=self.OVERLAY_LAYOUT)])
element_page = element_doc.pages[0]
element_body = PdfGenerator.get_element(element_page._page_box.all_children(), 'body')
element_body = element_body.copy_with_children(element_body.all_children())
element_html = PdfGenerator.get_element(element_page._page_box.all_children(), element)
if element == 'header':
element_height = element_html.height
if element == 'footer':
element_height = element_page.height - element_html.position_y
return element_body, element_height
def _apply_overlay_on_main(self, main_doc, header_body=None, footer_body=None):
"""
Insert the header and the footer in the main document.
Parameters
----------
main_doc: Document
The top level representation for a PDF page in Weasyprint.
header_body: BlockBox
A representation for an html element in Weasyprint.
footer_body: BlockBox
A representation for an html element in Weasyprint.
"""
for page in main_doc.pages:
page_body = PdfGenerator.get_element(page._page_box.all_children(), 'body')
if header_body:
page_body.children += header_body.all_children()
if footer_body:
page_body.children += footer_body.all_children()
def render_pdf(self):
"""
Returns
-------
pdf: a bytes sequence
The rendered PDF.
"""
if self.header_html:
header_body, header_height = self._compute_overlay_element('header')
else:
header_body, header_height = None, 0
if self.footer_html:
footer_body, footer_height = self._compute_overlay_element('footer')
else:
footer_body, footer_height = None, 0
margins = '{header_size}px {side_margin} {footer_size}px {side_margin}'.format(
header_size=header_height + self.extra_vertical_margin,
footer_size=footer_height + self.extra_vertical_margin,
side_margin=f'{self.side_margin}cm',
)
content_print_layout = '@page {size: A4 portrait; margin: %s;}' % margins
html = HTML(
string=self.main_html,
base_url=self.base_url,
)
main_doc = html.render(stylesheets=[CSS(string=content_print_layout)])
if self.header_html or self.footer_html:
self._apply_overlay_on_main(main_doc, header_body, footer_body)
pdf = main_doc.write_pdf()
return pdf
@staticmethod
def get_element(boxes, element):
"""
Given a set of boxes representing the elements of a PDF page in a DOM-like way, find the
box which is named `element`.
Look at the notes of the class for more details on Weasyprint insides.
"""
for box in boxes:
if box.element_tag == element:
return box
return PdfGenerator.get_element(box.all_children(), element)

View File

@@ -1,30 +0,0 @@
<html>
<head>
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<div class="content">
<h2>Conditions g&eacute;n&eacute;rales & particuli&egrave;res</h2>
{% for provision in draft.provisions %}
<div class="provision">
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
<p>{{ provision.body|safe }}</p>
</div>
{% endfor %}
<div class="footer">
<hr/>
<p>À {{ draft.location }} le {{ draft.date }}</p>
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p>
<table class="signatures">
<tr>
{% for party in draft.parties %}<td>{{ party.part|safe }}:</td>{% endfor %}
</tr>
</table>
</div>
</div>
</body>
</html>

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
{% include 'styles.css' %}
</style>
</head>
<body>
<hr/>
</body>
</html>

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,24 @@
@font-face { @font-face {
font-family: 'Century Schoolbook'; font-family: 'Century Schoolbook';
src: url('http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf'); src: url('{{ root_url }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf');
} }
@font-face { @font-face {
font-family: "Century Schoolbook"; font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBold.ttf"); src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBold.ttf");
font-weight: bold; font-weight: bold;
} }
@font-face { @font-face {
font-family: "Century Schoolbook"; font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf"); src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf");
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Century Schoolbook"; font-family: "Century Schoolbook";
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf"); src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf");
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
} }
@@ -28,10 +28,10 @@
margin: 2cm 2cm 2cm 2cm; margin: 2cm 2cm 2cm 2cm;
counter-increment: page; counter-increment: page;
@bottom-center { @bottom-center {
content: "© Cooper, Hillman & Toshi LLC - {{ draft.name }} - Page " counter(page) "/" counter(pages); content: "© Cooper, Hillman & Toshi LLC - {{ contract.name }} - Page " counter(page) "/" counter(pages);
font-size: 0.8em; font-size: 0.8em;
} }
background: url('http://{{ static_host }}/assets/watermark.png') no-repeat; background: url('{{ root_url }}/assets/watermark.png') no-repeat;
background-size:contain; background-size:contain;
} }

View File

@@ -1,7 +1,47 @@
from fastapi import APIRouter from beanie import PydanticObjectId
from fastapi import HTTPException, Depends
from ..core.routes import get_crud_router from ..core.routes import get_crud_router
from .models import ContractDraft from ..user.manager import get_current_user
from .models import ContractDraft, ContractDraftStatus
from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate) draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate)
del(draft_router.routes[0])
del(draft_router.routes[2])
@draft_router.post("/", response_description="Contract Draft added to the database")
async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key()
o = await ContractDraft(**item.dict()).create()
await o.check_is_ready()
return {"message": "Contract Draft added successfully", "id": o.id}
@draft_router.put("/{id}", response_description="Contract Draft record updated")
async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead:
req = {k: v for k, v in req.dict().items() if v is not None}
update_query = {"$set": {
field: value for field, value in req.items()
}}
item = await ContractDraft.get(id)
if not item:
raise HTTPException(
status_code=404,
detail="Contract Draft record not found!"
)
if item.status == ContractDraftStatus.published:
raise HTTPException(
status_code=400,
detail="Contract Draft has already been published"
)
await item.update(update_query)
await item.check_is_ready()
return ContractDraftRead(**item.dict())

View File

@@ -1,9 +1,9 @@
import datetime
from typing import List from typing import List
from pydantic import Field from pydantic import BaseModel, Field
from .models import ContractDraft, DraftProvision, Party from .models import ContractDraft, DraftProvision, DraftParty, Contract
from ..entity.models import Entity from ..entity.models import Entity
from ..core.schemas import Writer from ..core.schemas import Writer
@@ -15,13 +15,17 @@ class ContractDraftRead(ContractDraft):
class ContractDraftCreate(Writer): class ContractDraftCreate(Writer):
name: str name: str = Field(title='Nom')
title: str title: str = Field(title='Titre')
parties: List[Party] parties: List[DraftParty] = Field(title='Parties')
provisions: List[DraftProvision] provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field( variables: List[DictionaryEntry] = Field(
default=[], default=[],
format="dictionary", format="dictionary",
title='Variables'
) )
async def validate_foreign_key(self): async def validate_foreign_key(self):
@@ -34,3 +38,38 @@ class ContractDraftCreate(Writer):
class ContractDraftUpdate(ContractDraftCreate): class ContractDraftUpdate(ContractDraftCreate):
pass pass
class ForeignEntityRead(BaseModel):
label: str
class Config:
title = "Avocat"
class PartyRead(BaseModel):
signature_affixed: bool = Field(title='Signature apposée?')
signature_uuid: str = Field(format="signature-link", title="Lien vers signature")
part: str = Field(title='Rôle')
entity: ForeignEntityRead = Field(title='Client')
class Config:
title = "Partie"
class ContractRead(Contract):
parties: List[PartyRead]
lawyer: ForeignEntityRead
class Config:
title = "Contrat"
class ContractCreate(Writer):
date: datetime.date
location: str
draft_id: str
class ContractUpdate(BaseModel):
pass

View File

@@ -6,8 +6,8 @@ from pydantic import BaseModel, Field, validator
class CrudDocument(Document): class CrudDocument(Document):
_id: str _id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False) created_at: datetime = Field(default=datetime.utcnow(), nullable=False, title="Créé le")
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False) updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
@validator("label", always=True, check_fields=False) @validator("label", always=True, check_fields=False)
def generate_label(cls, v, values, **kwargs): def generate_label(cls, v, values, **kwargs):

View File

@@ -1,10 +1,13 @@
from beanie import PydanticObjectId from beanie import PydanticObjectId
from beanie.odm.operators.find.comparison import In
from beanie.operators import And, RegEx, Eq from beanie.operators import And, RegEx, Eq
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException, Depends
from fastapi_paginate import Page, Params, add_pagination from fastapi_paginate import Page, Params, add_pagination
from fastapi_paginate.ext.motor import paginate from fastapi_paginate.ext.motor import paginate
from ..user.manager import get_current_user, get_current_superuser
def parse_sort(sort_by): def parse_sort(sort_by):
if not sort_by: if not sort_by:
@@ -36,16 +39,21 @@ def parse_query(query: str, model):
or_array = [] or_array = []
for field in model.Settings.fulltext_search: for field in model.Settings.fulltext_search:
or_array.append(RegEx(field, value, 'i')) words_and_array = []
for word in value.split(' '):
words_and_array.append(RegEx(field, word, 'i'))
or_array.append(And(*words_and_array) if len(words_and_array) > 1 else words_and_array[0])
operand = Or(or_array) if len(or_array) > 1 else or_array[0] operand = Or(or_array) if len(or_array) > 1 else or_array[0]
elif operator == 'eq': elif operator == 'eq':
operand = Eq(column, value) operand = Eq(column, value)
elif operator == 'in':
operand = In(column, value.split(','))
and_array.append(operand) and_array.append(operand)
if and_array: if and_array:
return And(and_array) if len(and_array) > 1 else and_array[0] return And(*and_array) if len(and_array) > 1 else and_array[0]
else: else:
return {} return {}
@@ -55,18 +63,19 @@ def get_crud_router(model, model_create, model_read, model_update):
router = APIRouter() router = APIRouter()
@router.post("/", response_description="{} added to the database".format(model.__name__)) @router.post("/", response_description="{} added to the database".format(model.__name__))
async def create(item: model_create) -> dict: async def create(item: model_create, user=Depends(get_current_user)) -> dict:
await item.validate_foreign_key() await item.validate_foreign_key()
o = await model(**item.dict()).create() o = await model(**item.dict()).create()
return {"message": "{} added successfully".format(model.__name__), "id": o.id} return {"message": "{} added successfully".format(model.__name__), "id": o.id}
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) @router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
async def read_id(id: PydanticObjectId) -> model_read: async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
item = await model.get(id) item = await model.get(id)
return model_read(**item.dict()) return model_read(**item.dict())
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__)) @router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None) -> Page[model_read]: async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None,
user=Depends(get_current_user)) -> Page[model_read]:
sort = parse_sort(sort_by) sort = parse_sort(sort_by)
query = parse_query(query, model_read) query = parse_query(query, model_read)
@@ -75,7 +84,7 @@ def get_crud_router(model, model_create, model_read, model_update):
return await items return await items
@router.put("/{id}", response_description="{} record updated".format(model.__name__)) @router.put("/{id}", response_description="{} record updated".format(model.__name__))
async def update(id: PydanticObjectId, req: model_update) -> model_read: async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
req = {k: v for k, v in req.dict().items() if v is not None} req = {k: v for k, v in req.dict().items() if v is not None}
update_query = {"$set": { update_query = {"$set": {
field: value for field, value in req.items() field: value for field, value in req.items()
@@ -92,7 +101,7 @@ def get_crud_router(model, model_create, model_read, model_update):
return model_read(**item.dict()) return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) @router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
async def delete(id: PydanticObjectId) -> dict: async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
item = await model.get(id) item = await model.get(id)
if not item: if not item:

View File

@@ -5,10 +5,11 @@ from beanie import init_beanie
from .user import User, AccessToken from .user import User, AccessToken
from .entity.models import Entity from .entity.models import Entity
from .template.models import ContractTemplate, ProvisionTemplate from .template.models import ContractTemplate, ProvisionTemplate
from .order.models import Order from .contract.models import ContractDraft, Contract
from .contract.models import ContractDraft # from .order.models import Order
DATABASE_URL = "mongodb://root:example@mongo:27017/" DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06"
DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/"
async def init_db(): async def init_db():
@@ -17,5 +18,6 @@ async def init_db():
) )
await init_beanie(database=client.db_name, await init_beanie(database=client.db_name,
document_models=[User, AccessToken, Entity, ContractTemplate, ProvisionTemplate, ContractDraft, ], document_models=[User, AccessToken, Entity, ContractTemplate, ProvisionTemplate, ContractDraft,
Contract, ],
allow_index_dropping=True) allow_index_dropping=True)

View File

@@ -15,51 +15,68 @@ class EntityType(BaseModel):
class Individual(EntityType): class Individual(EntityType):
type: Literal['individual'] = 'individual' type: Literal['individual'] = 'individual'
firstname: Indexed(str) firstname: Indexed(str) = Field(title='Prénom')
middlename: Indexed(str) = "" middlename: Indexed(str) = Field(default="", title='Autres prénoms')
lastname: Indexed(str) lastname: Indexed(str) = Field(title='Nom de famille')
surnames: List[Indexed(str)] = [] surnames: List[Indexed(str)] = Field(
day_of_birth: date default=[],
place_of_birth: str = "" props={"items-per-row": "4", "numbered": True},
title="Surnoms"
)
day_of_birth: date = Field(default=None, title='Date de naissance')
place_of_birth: str = Field(default="", title='Lieu de naissance')
@property @property
def label(self) -> str: def label(self) -> str:
if len(self.surnames) > 0: # if len(self.surnames) > 0:
return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname) # return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
return '{} {}'.format(self.firstname, self.lastname) return '{} {}'.format(self.firstname, self.lastname)
class Config:
title = 'Particulier'
class Employee(BaseModel): class Employee(BaseModel):
role: Indexed(str) role: Indexed(str) = Field(title='Poste')
entity_id: str = Field(foreignKey={ entity_id: str = Field(foreignKey={
"reference": { "reference": {
"resource": "entity", "resource": "entity",
"schema": "Entity", "schema": "Entity",
"condition": "entity_data.type=individual" "condition": "entity_data.type=individual"
} }
}) },
title='Employé'
)
class Config:
title = 'Fiche Employé'
class Corporation(EntityType): class Corporation(EntityType):
type: Literal['corporation'] = 'corporation' type: Literal['corporation'] = 'corporation'
title: Indexed(str) title: Indexed(str) = Field(title='Dénomination sociale')
activity: Indexed(str) activity: Indexed(str) = Field(title='Activité')
employees: List[Employee] = Field(default=[]) employees: List[Employee] = Field(default=[], title='Employés')
class Config:
title = 'Entreprise'
class Institution(EntityType): class Institution(Corporation):
type: Literal['institution'] = 'institution' type: Literal['institution'] = 'institution'
title: Indexed(str)
activity: Indexed(str) class Config:
employees: List[Employee] = Field(default=[]) title = 'Institution'
class Entity(CrudDocument): class Entity(CrudDocument):
"""
Fiche d'un client
"""
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type') entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
label: str = None label: str = None
address: Optional[str] = "" address: str = Field(default="", title='Adresse')
@validator("label", always=True) @validator("label", always=True)
def generate_label(cls, v, values, **kwargs): def generate_label(cls, v, values, **kwargs):
@@ -75,3 +92,10 @@ class Entity(CrudDocument):
else datetime(year=dt.year, month=dt.month, day=dt.day, else datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0) hour=0, minute=0, second=0)
} }
class Config:
title = 'Client'
@classmethod
def get_create_resource(cls):
print('coucou')

View File

@@ -11,9 +11,11 @@ class EntityRead(Entity):
class EntityCreate(Writer): class EntityCreate(Writer):
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type') entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: Optional[str] = "" address: str = Field(default="", title='Adresse')
class Config:
title = "Création d'un client"
class EntityUpdate(BaseModel): class EntityUpdate(EntityCreate):
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type') pass
address: Optional[str] = ""

View File

@@ -4,8 +4,8 @@ from .contract import contract_router
from .db import init_db from .db import init_db
from .user import user_router, get_auth_router from .user import user_router, get_auth_router
from .entity import entity_router from .entity import entity_router
from .order import order_router
from .template import template_router from .template import template_router
# from .order import order_router
app = FastAPI(root_path="/api/v1") app = FastAPI(root_path="/api/v1")
@@ -15,17 +15,12 @@ async def on_startup():
await init_db() await init_db()
@app.get("/")
async def root():
return {"message": "Hello World"}
app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], ) app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], )
app.include_router(user_router, prefix="/users", tags=["users"], ) app.include_router(user_router, prefix="/users", tags=["users"], )
app.include_router(entity_router, prefix="/entity", tags=["entity"], ) app.include_router(entity_router, prefix="/entity", tags=["entity"], )
app.include_router(order_router, prefix="/order", tags=["order"], )
app.include_router(template_router, prefix="/template", tags=["template"], ) app.include_router(template_router, prefix="/template", tags=["template"], )
app.include_router(contract_router, prefix="/contract", tags=["contract"], ) app.include_router(contract_router, prefix="/contract", tags=["contract"], )
# app.include_router(order_router, prefix="/order", tags=["order"], )
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn

View File

@@ -14,9 +14,10 @@ class PartyTemplate(BaseModel):
"schema": "Entity", "schema": "Entity",
} }
}, },
default="" default="",
title="Partie"
) )
part: str part: str = Field(title="Rôle")
representative_id: str = Field( representative_id: str = Field(
foreignKey={ foreignKey={
"reference": { "reference": {
@@ -24,9 +25,13 @@ class PartyTemplate(BaseModel):
"schema": "Entity", "schema": "Entity",
} }
}, },
default="" default="",
title="Représentant"
) )
class Config:
title = 'Partie'
def remove_html_tags(text): def remove_html_tags(text):
"""Remove html tags from a string""" """Remove html tags from a string"""
@@ -36,10 +41,14 @@ def remove_html_tags(text):
class ProvisionTemplate(CrudDocument): class ProvisionTemplate(CrudDocument):
name: str """
title: str = RichtextSingleline() Modèle de clause à décliner
"""
name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre")
label: str = "" label: str = ""
body: str = RichtextMultiline() body: str = RichtextMultiline(title="Corps")
@validator("label", always=True) @validator("label", always=True)
def generate_label(cls, v, values, **kwargs): def generate_label(cls, v, values, **kwargs):
@@ -48,6 +57,9 @@ class ProvisionTemplate(CrudDocument):
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title', 'body'] fulltext_search = ['name', 'title', 'body']
class Config:
title = 'Template de clause'
class ProvisionTemplateReference(BaseModel): class ProvisionTemplateReference(BaseModel):
provision_template_id: str = Field( provision_template_id: str = Field(
@@ -58,22 +70,31 @@ class ProvisionTemplateReference(BaseModel):
"displayedFields": ['title', 'body'] "displayedFields": ['title', 'body']
}, },
}, },
props={"parametrized": True} props={"parametrized": True},
title="Template de clause"
) )
class Config:
title = 'Clause'
class ContractTemplate(CrudDocument): class ContractTemplate(CrudDocument):
name: str """
title: str Modèle de contrat à décliner
"""
name: str = Field(title="Nom")
title: str = Field(title="Titre")
label: str = "" label: str = ""
parties: List[PartyTemplate] = [] parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field( provisions: List[ProvisionTemplateReference] = Field(
default=[], default=[],
props={"items-per-row": "1", "numbered": True} props={"items-per-row": "1", "numbered": True},
title="Clauses"
) )
variables: List[DictionaryEntry] = Field( variables: List[DictionaryEntry] = Field(
default=[], default=[],
format="dictionary", format="dictionary",
title="Variables"
) )
@validator("label", always=True) @validator("label", always=True)
@@ -82,3 +103,6 @@ class ContractTemplate(CrudDocument):
class Settings(CrudDocument.Settings): class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title'] fulltext_search = ['name', 'title']
class Config:
title = 'Template de contrat'

View File

@@ -11,15 +11,23 @@ class ContractTemplateRead(ContractTemplate):
class ContractTemplateCreate(Writer): class ContractTemplateCreate(Writer):
name: str name: str = Field(title="Nom")
title: str title: str = Field(title="Titre")
parties: List[PartyTemplate] = [] parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items-per-row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field( variables: List[DictionaryEntry] = Field(
default=[], default=[],
format="dictionary", format="dictionary",
props={"required": False} props={"required": False},
title="Variables"
) )
provisions: List[ProvisionTemplateReference] = []
class Config:
title = 'Template de Contrat'
class ContractTemplateUpdate(ContractTemplateCreate): class ContractTemplateUpdate(ContractTemplateCreate):
@@ -31,9 +39,12 @@ class ProvisionTemplateRead(ProvisionTemplate):
class ProvisionTemplateCreate(Writer): class ProvisionTemplateCreate(Writer):
name: str name: str = Field(title="Nom")
title: str = RichtextSingleline() title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline() body: str = RichtextMultiline(title="Corps")
class Config:
title = 'Template de Clause'
class ProvisionTemplateUpdate(ProvisionTemplateCreate): class ProvisionTemplateUpdate(ProvisionTemplateCreate):

View File

@@ -1,3 +1,3 @@
from .routes import router as user_router, get_auth_router from .routes import router as user_router
from .manager import get_auth_router
from .models import User, AccessToken from .models import User, AccessToken

View File

@@ -1,5 +1,5 @@
import uuid import uuid
from typing import Any, Dict, Generic, Optional from typing import Any
from bson import ObjectId from bson import ObjectId
from fastapi import Depends from fastapi import Depends
@@ -88,10 +88,10 @@ async def get_user_manager(user_db=Depends(get_user_db)):
def get_database_strategy( def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> DatabaseStrategy: ) -> DatabaseStrategy:
return DatabaseStrategy(access_token_db, lifetime_seconds=3600) return DatabaseStrategy(access_token_db, lifetime_seconds=None)
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") bearer_transport = BearerTransport(tokenUrl="auth/login")
auth_backend = AuthenticationBackend( auth_backend = AuthenticationBackend(
@@ -107,6 +107,7 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
) )
get_current_user = fastapi_users.current_user(active=True) get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
def get_auth_router(): def get_auth_router():

View File

@@ -1,6 +1,6 @@
from typing import Optional, TypeVar from typing import Optional, TypeVar
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field from pydantic import Field
from beanie import PydanticObjectId from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
@@ -15,6 +15,7 @@ class AccessToken(BeanieBaseAccessToken[PydanticObjectId]):
class User(BeanieBaseUser[PydanticObjectId]): class User(BeanieBaseUser[PydanticObjectId]):
login: str login: str
entity_id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False) created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False) updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)

View File

@@ -7,15 +7,15 @@ from typing import List
from .models import User from .models import User
from .schemas import UserRead, UserUpdate, UserCreate from .schemas import UserRead, UserUpdate, UserCreate
from .manager import get_user_manager, get_current_user, get_auth_router from .manager import get_user_manager, get_current_user, get_current_superuser
router = APIRouter() router = APIRouter()
@router.post("/", response_description="User added to the database") @router.post("/", response_description="User added to the database")
async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> dict: async def create(user_form: UserCreate, user_manager=Depends(get_user_manager), user=Depends(get_current_superuser)) -> dict:
await user_manager.create(user, safe=True) await user_manager.create(user_form, safe=True)
return {"message": "User added successfully"} return {"message": "User added successfully"}
@@ -26,22 +26,22 @@ async def read_me(user=Depends(get_current_user)) -> UserRead:
@router.get("/{id}", response_description="User record retrieved") @router.get("/{id}", response_description="User record retrieved")
async def read_id(id: PydanticObjectId) -> UserRead: async def read_id(id: PydanticObjectId, user=Depends(get_current_superuser)) -> UserRead:
user = await User.get(id) user = await User.get(id)
return UserRead(**user.dict()) return UserRead(**user.dict())
@router.get("/", response_model=List[UserRead], response_description="User records retrieved") @router.get("/", response_model=List[UserRead], response_description="User records retrieved")
async def read_list() -> List[UserRead]: async def read_list(user=Depends(get_current_superuser)) -> List[UserRead]:
users = await User.find_all().to_list() users = await User.find_all().to_list()
return users return users
@router.put("/{id}", response_description="User record updated") @router.put("/{id}", response_description="User record updated")
async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead: async def update(id: PydanticObjectId, user_form: UserUpdate, user=Depends(get_current_superuser)) -> UserRead:
req = {k: v for k, v in req.dict().items() if v is not None} user_form = {k: v for k, v in user_form.dict().items() if v is not None}
update_query = {"$set": { update_query = {"$set": {
field: value for field, value in req.items() field: value for field, value in user_form.items()
}} }}
user = await User.get(id) user = await User.get(id)
@@ -56,7 +56,7 @@ async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead:
@router.delete("/{id}", response_description="User record deleted from the database") @router.delete("/{id}", response_description="User record deleted from the database")
async def delete(id: PydanticObjectId) -> dict: async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
record = await User.get(id) record = await User.get(id)
if not record: if not record:

View File

@@ -1,10 +1,6 @@
import uuid from pydantic import BaseModel
from typing import TypeVar
from pydantic import BaseModel, Field
from fastapi_users import schemas from fastapi_users import schemas
from ..core.schemas import Reader
from .models import User from .models import User
@@ -24,6 +20,7 @@ class UserCreate(UserBase):
login: str login: str
password: str password: str
email: str email: str
entity_id: str
class UserUpdate(UserBase): class UserUpdate(UserBase):

View File

View File

View File

@@ -3,10 +3,10 @@ import asyncio
import json import json
from os import path from os import path
from app.db import init_db, Entity, Order, Contract, User, AccessToken from app.db import init_db, Entity, Contract, ContractTemplate, ProvisionTemplate, User
models = [Entity, Order, Contract, User] models = [Entity, Contract, User, ContractTemplate, ProvisionTemplate]
async def handle_migration(args): async def handle_migration(args):

27
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,27 @@
version: "3.9"
services:
back:
image: git.dorfsvald.net/ewandor/cht-lawfirm-back-prod:latest
restart: always
volumes:
- ${ROOT_PATH}/back/media:/code/media
nginx:
image: git.dorfsvald.net/ewandor/cht-lawfirm-nginx-prod:latest
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.19"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -0,0 +1,42 @@
version: "3.9"
services:
back:
build:
context: ${ROOT_PATH}/back
restart: always
ports:
- "8000:8000"
volumes:
- ${ROOT_PATH}/back/app:/code/app
- ${ROOT_PATH}/back/media:/code/media
front:
build:
context: ${ROOT_PATH}/front
restart: always
ports:
- "4200:4200"
volumes:
- ${ROOT_PATH}/front/app/src:/app/src
- ${ROOT_PATH}/front/app/public:/app/public
nginx:
build:
context: ${ROOT_PATH}/nginx
restart: always
ports:
- "3820:80"
mongo:
image: "mongo:4.4.18"
restart: always
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes:
- database:/data/db
volumes:
database:

View File

@@ -3,15 +3,18 @@ services:
back: back:
build: build:
context: ./back context: ./back
image: cht-lawfirm-back-dev
restart: always restart: always
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./back/app:/code/app - ./back/app:/code/app
- ./back/media:/code/media
front: front:
build: build:
context: ./front context: ./front
image: cht-lawfirm-front-dev
restart: always restart: always
ports: ports:
- "4200:4200" - "4200:4200"
@@ -22,18 +25,19 @@ services:
nginx: nginx:
build: build:
context: ./nginx context: ./nginx
image: cht-lawfirm-nginx-dev
restart: always restart: always
ports: ports:
- "80:80" - "80:80"
mongo: mongo:
image: "mongo" image: "mongo:4.4.19"
restart: always restart: always
ports: ports:
- "27017:27017" - "27017:27017"
environment: environment:
MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
volumes: volumes:
- database:/data/db - database:/data/db

View File

@@ -1,22 +1,11 @@
FROM node:lts-alpine FROM node:lts-alpine
# install simple http server for serving static content
RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app WORKDIR /app
RUN npm install -g @angular/cli http-server
# copy both 'package.json' and 'package-lock.json' (if available)
COPY app/package*.json ./ COPY app/package*.json ./
# install project dependencies
RUN npm install -g @angular/cli
RUN npm install RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY app/ . COPY app/ .
# build app for production with minification
RUN npm run build RUN npm run build
EXPOSE 4200 EXPOSE 4200

17
front/Dockerfile.prod Normal file
View File

@@ -0,0 +1,17 @@
FROM node:lts-alpine AS builder
WORKDIR /app
RUN npm install -g @angular/cli
COPY app/package*.json ./
RUN npm install
COPY app/ .
RUN npm run build --prod
FROM nginx:alpine
COPY nginx.prod.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist/app/fr/ /usr/share/nginx/html/

View File

@@ -9,10 +9,21 @@
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "app", "prefix": "app",
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": ""
}
}
},
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"localize": ["fr"],
"i18nMissingTranslation": "warning",
"outputPath": "dist/app", "outputPath": "dist/app",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
@@ -65,14 +76,22 @@
}, },
"development": { "development": {
"browserTarget": "app:build:development" "browserTarget": "app:build:development"
},
"fr": {
"browserTarget": "app:build:fr"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
"options": { "options": {
"browserTarget": "app:build" "browserTarget": "app:build",
"format": "xlf2",
"outputPath": "src/locale",
"targetFiles": [
"messages.fr.xlf"
]
} }
}, },
"test": { "test": {

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^15.0.0", "@angular/animations": "^15.0.0",
"@angular/cdk": "^15.2.1",
"@angular/common": "^15.0.0", "@angular/common": "^15.0.0",
"@angular/compiler": "^15.0.0", "@angular/compiler": "^15.0.0",
"@angular/core": "^15.0.0", "@angular/core": "^15.0.0",
@@ -23,6 +24,8 @@
"@ngx-formly/core": "^6.0.0", "@ngx-formly/core": "^6.0.0",
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"@tinymce/tinymce-angular": "^7.0.0", "@tinymce/tinymce-angular": "^7.0.0",
"@types/fabric": "^5.3.0",
"fabric": "^5.3.0",
"ngx-bootstrap-icons": "^1.9.1", "ngx-bootstrap-icons": "^1.9.1",
"ngx-wig": "^15.1.4", "ngx-wig": "^15.1.4",
"rxjs": "~7.5.0", "rxjs": "~7.5.0",
@@ -42,6 +45,7 @@
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0", "karma-jasmine-html-reporter": "~2.0.0",
"ng-extract-i18n-merge": "^2.5.1",
"typescript": "~4.8.2" "typescript": "~4.8.2"
} }
} }

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import {ContractsModule} from "./views/contracts/contracts.module";
const routes: Routes = [ const routes: Routes = [
{ {
@@ -32,6 +31,11 @@ const routes: Routes = [
}, },
{ {
path: 'contract-drafts', path: 'contract-drafts',
loadChildren: () =>
import('./views/contract-drafts/contract-drafts.module').then((m) => m.ContractDraftsModule)
},
{
path: 'contracts',
loadChildren: () => loadChildren: () =>
import('./views/contracts/contracts.module').then((m) => m.ContractsModule) import('./views/contracts/contracts.module').then((m) => m.ContractsModule)
}, },

View File

@@ -0,0 +1,5 @@
.sidenav {
background-image: url("/assets/leather_texture.png");
background-size: 220px;
max-width: 220px;
}

View File

@@ -3,7 +3,7 @@
<main style="margin-top: 0px;"> <main style="margin-top: 0px;">
<div class="container-fluid"> <div class="container-fluid">
<div class="row flex-nowrap"> <div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark"> <div class="sidenav col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
<sidenav class="sticky-top"></sidenav> <sidenav class="sticky-top"></sidenav>
</div> </div>
<div class="col py-3"> <div class="col py-3">
@@ -13,3 +13,4 @@
</div> </div>
</div> </div>
</main> </main>

View File

@@ -1,10 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {Title} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent { export class AppComponent {
title = 'app'; title = 'Cooper, Hillman & Toshi';
constructor(private titleService: Title) {
titleService.setTitle(this.title)
}
} }

View File

@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'; import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons';
import { ReactiveFormsModule } from "@angular/forms";
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@@ -9,20 +11,31 @@ import { AppComponent } from './app.component';
import { SidenavComponent } from "./layout/sidenav/sidenav.component"; import { SidenavComponent } from "./layout/sidenav/sidenav.component";
import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component"; import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component";
import { FlashmessagesService } from "./layout/flashmessages/flashmessages.service"; import { FlashmessagesService } from "./layout/flashmessages/flashmessages.service";
import { LoginComponent, LogoutComponent } from "./layout/auth/auth.component";
import { AuthService } from "./layout/auth/auth.service";
import { AuthInterceptor } from "./layout/auth/auth.interceptor"
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
SidenavComponent, SidenavComponent,
FlashmessagesComponent FlashmessagesComponent,
LoginComponent,
LogoutComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
HttpClientModule,
],
providers: [
FlashmessagesService,
AuthService,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
], ],
providers: [FlashmessagesService],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }

View File

@@ -0,0 +1,126 @@
import {Component, ElementRef, EventEmitter, Output, ViewChild} from "@angular/core";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {Router} from "@angular/router";
import {AuthService} from "./auth.service";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
@Component({
selector: 'login',
template: `
<button *ngIf="!isAuthenticated"
class="nav-link px-3 w-100"
(click)="openLoginModal()"
><i-bs name="key-fill"/><span class="ms-1 d-none d-sm-inline" i18n>Login</span></button>
<ng-template #loginModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Login</h4>
<button type="button" class="btn-close" aria-label="Close"
(click)="modal.dismiss('Cross click')"></button>
</div>
<div class="modal-body">
<form [formGroup]="form">
<fieldset>
<div class="form-field">
<label i18n>Username:</label>
<input class="form-control" name="username" formControlName="username">
</div>
<div class="form-field">
<label i18n>Password:</label>
<input class="form-control" name="password" formControlName="password"
type="password">
</div>
</fieldset>
</form>
</div>
<div class="modal-footer">
<div class="form-buttons">
<button class="btn btn-primary"
(click)="this.login()" i18n>Login</button>
</div>
<div class="form-buttons">
<button class="btn btn-danger"
(click)="modal.dismiss('Cancel click')" i18n>Cancel</button>
</div>
</div>
</ng-template>
`
})
export class LoginComponent {
@Output() loginSuccess = new EventEmitter<string>()
@ViewChild('loginModal')
loginModal!: ElementRef;
form: FormGroup;
isAuthenticated: boolean = false
constructor(private fb:FormBuilder,
private authService: AuthService,
private modalService: NgbModal
) {
this.form = this.fb.group({
username: ['',Validators.required],
password: ['',Validators.required]
});
this.authService.onAuthenticationRequired$.subscribe((required: boolean) => {
if (required) {
this.openLoginModal();
}
})
this.isAuthenticated = this.authService.is_authenticated;
this.authService.onIsAuthenticated$.subscribe((isAuthenticated: boolean) => {
this.isAuthenticated = isAuthenticated;
})
}
openLoginModal() {
this.modalService.open(this.loginModal, { ariaLabelledBy: 'modal-basic-title' }).result.then(
);
}
login() {
const val = this.form.value;
if (val.username && val.password) {
this.authService.login(val.username, val.password)
.subscribe((answer: string) => {
this.form.reset();
this.modalService.dismissAll();
this.loginSuccess.emit(answer)
}
);
}
}
}
@Component({
selector: 'logout',
template: `
<button *ngIf="isAuthenticated"
class="nav-link px-3 w-100"
(click)="logout()"
><i-bs name="door-open-fill"/><span class="ms-1 d-none d-sm-inline" i18n>Logout</span></button>
`
})
export class LogoutComponent {
@Output() logoutSuccess = new EventEmitter()
isAuthenticated: boolean = false
constructor(private authService: AuthService) {
this.isAuthenticated = this.authService.is_authenticated;
this.authService.onIsAuthenticated$.subscribe((isAuthenticated: boolean) => {
this.isAuthenticated = isAuthenticated
})
}
logout() {
this.authService.logout()
.subscribe(() => {
this.logoutSuccess.emit()
}
);
}
}

View File

@@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { HttpErrorResponse, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Observable, throwError } from 'rxjs';
import { catchError } from "rxjs/operators";
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const authToken = this.auth.getAuthorizationToken();
let authReq: HttpRequest<any>;
if (authToken) {
authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
} else {
authReq = req;
}
return next.handle(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status && error.status == 401) {
this.auth.requestAuth();
return throwError(() => $localize`Authentication required`);
}
if (error.status && error.status == 403) {
return throwError(() => $localize`Permissions too low`);
}
return throwError(() => error);
})
);
}
}

View File

@@ -0,0 +1,75 @@
import { Injectable } from "@angular/core";
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
import { map } from "rxjs/operators";
import { Subject } from "rxjs";
import { FlashmessagesService } from "../flashmessages/flashmessages.service";
export interface token_answer {
access_token: string,
token_type: string
}
@Injectable()
export class AuthService {
private access_token: string | null;
private authenticationRequired = new Subject<boolean>();
onAuthenticationRequired$ = this.authenticationRequired.asObservable();
is_authenticated = false;
private isAuthenticated = new Subject<boolean>();
onIsAuthenticated$ = this.isAuthenticated.asObservable();
constructor(private http: HttpClient,
private flashMessage: FlashmessagesService) {
this.access_token = localStorage.getItem('authtoken')
this.is_authenticated = Boolean(this.access_token)
this.isAuthenticated.next(this.is_authenticated)
}
login(username:string, password:string ) {
const body = new HttpParams()
.set('username', username)
.set('password', password);
return this.http.post<token_answer>('/api/v1/auth/login', body.toString(),
{
headers: new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded')
}).pipe(map( v => {
localStorage.setItem('authtoken', v.access_token);
this.access_token = v.access_token
this.flashMessage.success('Login successful. Welcome ' + username);
this.is_authenticated = true;
this.isAuthenticated.next(this.is_authenticated)
return username
} ));
}
logout() {
return this.http.post<token_answer>('/api/v1/auth/logout', '',
{
headers: new HttpHeaders()
}).pipe(map( v => {
localStorage.removeItem('authtoken');
this.access_token = null;
this.flashMessage.success('Logout successful. Goodbye');
this.is_authenticated = false;
this.isAuthenticated.next(this.is_authenticated)
} ));
}
getAuthorizationToken() {
return `Bearer ${this.access_token}`;
}
requestAuth() {
localStorage.removeItem('authtoken');
this.access_token = null;
this.authenticationRequired.next(true);
this.is_authenticated = false;
this.isAuthenticated.next(this.is_authenticated)
}
}

View File

@@ -1,7 +1,7 @@
<div class="toast-container position-fixed bottom-0 end-0 p-3"> <div class="toast-container position-fixed bottom-0 end-0 p-3">
<ngb-toast <ngb-toast
*ngFor="let flashmessage of flashmessagesService.toasts" *ngFor="let flashmessage of flashmessagesService.toasts"
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 1500" [header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 5000"
(hiddden)="flashmessagesService.remove(flashmessage)" (hiddden)="flashmessagesService.remove(flashmessage)"
> >
<ng-container [ngSwitch]="flashmessage.type"> <ng-container [ngSwitch]="flashmessage.type">

View File

@@ -0,0 +1,3 @@
.logout {
margin-top: 25px;
}

View File

@@ -1,12 +1,16 @@
<div class="pt-2 text-white min-vh-100 text-nowrap"> <div class="pt-2 text-white min-vh-100 text-nowrap">
<a routerLink="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none"> <a routerLink="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<span class="fs-5 d-none d-sm-inline">Cooper, Hillman &<br/>Toshi LLP</span> <img class="fs-5 d-none d-sm-inline w-100" src="/assets/logo.png" alt="Cooper, Hillman & Toshi LLP">
</a> </a>
<ul class="nav nav-pills flex-column align-items-sm-start mb-sm-auto mb-0 w-100" id="menu"> <ul class="nav nav-pills flex-column align-items-sm-start mb-sm-auto mb-0 w-100" id="menu">
<li *ngFor="let item of Menu" class="nav-item w-100"> <li><login></login></li>
<a class="nav-link px-3 w-100" routerLink="{{item.link}}" [class.active]="is_current_page(item)"> <ng-container *ngIf="isAuthenticated">
<i-bs [name]="item.icon"></i-bs><span class="ms-1 d-none d-sm-inline" [innerHTML]="item.title"></span> <li *ngFor="let item of Menu" class="nav-item w-100">
</a> <a class="nav-link px-3 w-100" routerLink="{{item.link}}" [class.active]="is_current_page(item)">
</li> <i-bs [name]="item.icon"></i-bs><span class="ms-1 d-none d-sm-inline" [innerHTML]="item.title"></span>
</a>
</li>
</ng-container>
<li class="logout"><logout></logout></li>
</ul> </ul>
</div> </div>

View File

@@ -1,6 +1,15 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { IconNamesEnum } from "ngx-bootstrap-icons"; import { IconNamesEnum } from "ngx-bootstrap-icons";
import { AuthService } from "../auth/auth.service";
interface MenuItem {
title: string,
link: string,
icon: IconNamesEnum
}
@Component({ @Component({
selector: "sidenav", selector: "sidenav",
@@ -8,37 +17,52 @@ import { IconNamesEnum } from "ngx-bootstrap-icons";
styleUrls: ["./sidenav.component.css"] styleUrls: ["./sidenav.component.css"]
}) })
export class SidenavComponent { export class SidenavComponent {
Menu = [ Menu: MenuItem[] = [
{ {
title: "Dashboard", title: $localize`Dashboard`,
link: "/dashboard", link: "/dashboard",
icon: IconNamesEnum.HouseFill icon: IconNamesEnum.HouseFill
}, },
{ {
title: "Entities", title: $localize`Entities`,
link: "/entities", link: "/entities",
icon: IconNamesEnum.PeopleFill icon: IconNamesEnum.PeopleFill
}, },
{ {
title: "Provision&nbsp;Templates", title: $localize`Provision&nbsp;Templates`,
link: "/templates/provisions", link: "/templates/provisions",
icon: IconNamesEnum.BlockquoteLeft icon: IconNamesEnum.BlockquoteLeft
}, },
{ {
title: "Contracts&nbsp;Templates", title: $localize`Contracts&nbsp;Templates`,
link: "/templates/contracts", link: "/templates/contracts",
icon: IconNamesEnum.FileCodeFill icon: IconNamesEnum.FileCodeFill
}, },
{ {
title: "Contracts&nbsp;Drafts", title: $localize`Contracts&nbsp;Drafts`,
link: "/contract-drafts", link: "/contract-drafts",
icon: IconNamesEnum.PencilSquare icon: IconNamesEnum.PencilSquare
}, },
{
title: $localize`Contracts`,
link: "/contracts",
icon: IconNamesEnum.FileEarmarkTextFill
},
] ]
constructor(private router: Router) {} isAuthenticated: boolean = false
is_current_page(menu_item: any) { constructor(
return this.router.url.indexOf(menu_item.link) > -1; private router: Router,
private authService: AuthService,) {
this.isAuthenticated = this.authService.is_authenticated;
this.authService.onIsAuthenticated$.subscribe((isAuthenticated: boolean) => {
this.isAuthenticated = isAuthenticated;
})
}
is_current_page(menu_item: MenuItem) {
return this.router.url.startsWith(menu_item.link);
} }
} }

View File

@@ -4,5 +4,6 @@
[schema]="this.schema" [schema]="this.schema"
(resourceUpdated)="this.flashService.success('Entity updated')" (resourceUpdated)="this.flashService.success('Entity updated')"
(resourceDeleted)="this.flashService.success('Entity deleted')" (resourceDeleted)="this.flashService.success('Entity deleted')"
(resourceReceived)="this.onResourceReceived($event)"
(error)="this.flashService.error($event)"> (error)="this.flashService.error($event)">
</crud-card> </crud-card>

View File

@@ -1,26 +1,32 @@
import {Component, Input} from "@angular/core"; import {Component, EventEmitter, Input, Output} from "@angular/core";
import {ActivatedRoute, ParamMap} from "@angular/router"; import {ActivatedRoute, ParamMap} from "@angular/router";
import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service"; import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service";
@Component({ @Component({
templateUrl: 'card.component.html', templateUrl: 'card.component.html',
selector: 'base-card', selector: 'base-card',
}) })
export class BaseCrudCardComponent { export class BaseCrudCardComponent {
@Input() resource: string | undefined; @Input() resource: string | undefined;
@Input() resource_id: string | null = null; @Input() resource_id: string | null = null;
@Input() schema: string | undefined; @Input() schema: string | undefined;
constructor( @Output() resourceReceived: EventEmitter<any> = new EventEmitter();
private route: ActivatedRoute,
public flashService: FlashmessagesService
) {}
ngOnInit(): void { constructor(
if (this.resource_id === null) { private route: ActivatedRoute,
this.route.paramMap.subscribe((params: ParamMap) => { public flashService: FlashmessagesService
this.resource_id = params.get('id') ) {}
})
ngOnInit(): void {
if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id')
})
}
} }
}
} onResourceReceived(model: any) {
this.resourceReceived.emit(model);
}
}

View File

@@ -2,6 +2,7 @@
[resource]="this.resource" [resource]="this.resource"
[schema]="this.schema" [schema]="this.schema"
[columns]="this.columns" [columns]="this.columns"
[filters]="this.filters"
(result)="this.flashService.success($event)" (result)="this.flashService.success($event)"
(error)="this.flashService.error($event)"> (error)="this.flashService.error($event)">
</crud-list> </crud-list>

View File

@@ -9,6 +9,7 @@ export class BaseCrudListComponent {
@Input() resource: string = ""; @Input() resource: string = "";
@Input() columns: string[] = []; @Input() columns: string[] = [];
@Input() schema: string | undefined; @Input() schema: string | undefined;
@Input() filters: string[] = [];
constructor( constructor(
public flashService: FlashmessagesService public flashService: FlashmessagesService

View File

@@ -1,4 +1,4 @@
<base-card <base-card
[resource]="this.resource" [resource]="this.resource"
[schema]="this.schema"> [schema]="this.schema">
</base-card>' </base-card>

View File

@@ -1,5 +1,6 @@
<base-list <base-list
[resource]="this.resource" [resource]="this.resource"
[schema]="this.schema" [schema]="this.schema"
[columns]="this.columns"> [columns]="this.columns"
[filters]="this.filters">
</base-list> </base-list>

View File

@@ -0,0 +1,44 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent } from "./drafts.component";
const routes: Routes = [
{
path: '',
data: {
title: 'Contract Drafts',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: DraftsNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftsCardComponent,
data: {
title: 'Card',
},
},
],
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ContractDraftsRoutingModule {}

View File

@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module";
import { ContractDraftsRoutingModule } from './contract-drafts-routing.module';
import { DraftsCardComponent, DraftsListComponent, DraftsNewComponent, DraftsNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service";
import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
import { ClipboardModule } from "@angular/cdk/clipboard";
@NgModule({
imports: [
CommonModule,
BaseViewModule,
ContractDraftsRoutingModule,
NgbAccordionModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
FormlyModule.forRoot({
types: [
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
]
}),
FormlyBootstrapModule,
ClipboardModule,
],
declarations: [
DraftsListComponent,
DraftsNewComponent,
DraftsCardComponent,
DraftsNewFormComponent
],
providers: [CrudService, ImageUploaderCrudService]
})
export class ContractDraftsModule {
}

View File

@@ -0,0 +1,173 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormlyFieldConfig } from "@ngx-formly/core";
import { FormGroup} from "@angular/forms";
import { CrudFormlyJsonschemaService } from "@common/crud/crud-formly-jsonschema.service";
import { CrudService } from "@common/crud/crud.service";
import { ActivatedRoute, ParamMap, Router } from "@angular/router";
import { formatDate } from "@angular/common";
import {FlashmessagesService} from "../../layout/flashmessages/flashmessages.service";
export class BaseDraftsComponent {
protected resource: string = "contract/draft";
protected schema: string = "ContractDraft";
}
@Component({
templateUrl: '../base-view/templates/list.template.html'
})
export class DraftsListComponent extends BaseDraftsComponent {
columns = ['status', 'name', 'title', 'parties.items.part'];
filters = ['status'];
}
@Component({
selector: 'draft-new-form',
template: `<base-new [resource]="this.resource" [schema]="this.schema" [model]="this.value"></base-new>`
})
export class DraftsNewFormComponent extends BaseDraftsComponent {
@Input() value: {} = {};
}
@Component({
selector: 'draft-new',
template: `
<formly-form [fields]="temaplateFormfields" [form]="temaplateForm"></formly-form>
<draft-new-form [value]="this.templateModel"></draft-new-form>
`
})
export class DraftsNewComponent extends BaseDraftsComponent implements OnInit {
templateModel: {} = {};
temaplateFormfields: FormlyFieldConfig[] = [];
temaplateForm: FormGroup = new FormGroup({});
fieldJson = {
type: "object",
properties: {
template_id: {
type: "string",
title: "Find a template",
foreignKey: {
reference: {
resource: "template/contract",
schema: "ContractTemplate"
}
}
}
},
}
constructor(private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService
) {
super();
}
ngOnInit() {
// @ts-ignore
this.temaplateFormfields = [this.formlyJsonschema.toFieldConfig(this.fieldJson)];
this.temaplateForm.valueChanges.subscribe((values) => {
if (values.template_id !== undefined) {
this.crudService.get("template/contract", values.template_id).subscribe((templateModel) => {
delete templateModel._id;
delete templateModel.created_at;
delete templateModel.updated_at;
delete templateModel.label;
const provisions = [];
for (const p of templateModel.provisions) {
provisions.push({
provision: { type: "template", provision_template_id: p.provision_template_id}
})
}
templateModel.provisions = provisions;
this.templateModel = templateModel;
});
} else {
this.templateModel = {};
}
})
}
}
@Component({
template: `
<base-card
[resource]="this.resource"
[schema]="this.schema"
(resourceReceived)="this.onResourceReceived($event)"
>
</base-card>
<a class="btn btn-link" href="/api/v1/contract/print/preview/draft/{{this.resource_id}}" target="_blank" i18n>Preview</a>
<ng-container *ngIf="this.isReadyForPublication;">
<formly-form [fields]="newContractFormfields" [form]="newContractForm" [model]="newContractModel"></formly-form>
<button class="btn btn-success" (click)="publish()" i18n>Publish</button>
</ng-container>
`
})
export class DraftsCardComponent extends BaseDraftsComponent implements OnInit {
resource_id: string | null = null;
templateModel: {} = {};
isReadyForPublication = false;
newContractFormfields: FormlyFieldConfig[] = [];
newContractForm: FormGroup = new FormGroup({});
newContractModel: any = {
date: formatDate(new Date(), 'YYYY-MM-dd', 'EN_US', 'CET'),
location: "Los Santos, SA",
draft_id: null
}
fieldJson = {
type: "object",
required: ["date", "location", "draft_id"],
properties: {
date: {
type: "string",
format: "date",
title: "Date"
},
location: {
type: "string",
title: "Location"
},
draft_id: {
type: "string",
title: "Contract Draft",
hidden: true
},
},
}
constructor(
private route: ActivatedRoute,
private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService,
private router: Router,
private flashService: FlashmessagesService,
) {
super();
}
ngOnInit(): void {
if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id')
})
}
this.newContractModel.draft_id = this.resource_id;
// @ts-ignore
this.newContractFormfields = [this.formlyJsonschema.toFieldConfig(this.fieldJson)];
}
publish() {
this.crudService.create('contract', this.newContractModel).subscribe({
next: (response: any) => this.router.navigate([`../../contracts/${response.id}`], {relativeTo: this.route}),
error: (err) => this.flashService.error(err)
});
}
onResourceReceived(model: any): void {
this.isReadyForPublication = model.status == "ready";
}
}

View File

@@ -1,44 +1,52 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { DraftCardComponent, DraftListComponent, DraftNewComponent } from "./drafts.component"; import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
data: {
title: 'Entities',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftListComponent,
data: { data: {
title: 'List', title: 'Contracts',
}, },
}, children: [
{ { path: '', redirectTo: 'list', pathMatch: 'full' },
path: 'new', { path: 'drafts', redirectTo: '/contract-drafts/list' },
component: DraftNewComponent, {
data: { path: 'list',
title: 'New', component: ContractsListComponent,
}, data: {
}, title: 'List',
{ },
path: ':id', },
component: DraftCardComponent, {
data: { path: 'new',
title: 'Card', component: ContractsNewComponent,
}, data: {
}, title: 'New',
], },
}, },
{
path: 'signature/:id',
component: ContractsSignatureComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: ContractsCardComponent,
data: {
title: 'Card',
},
}
]
}
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class ContractsRoutingModule {} export class ContractsRoutingModule {}

View File

@@ -0,0 +1,152 @@
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
import { ImageUploaderCrudService } from "@common/crud/crud.service";
export class BaseContractsComponent {
protected resource: string = "contract";
protected schema: string = "Contract";
}
@Component({
templateUrl: '../base-view/templates/list.template.html'
})
export class ContractsListComponent extends BaseContractsComponent {
columns = ['status', 'name', 'title', 'parties.items.entity.label', 'lawyer.label', 'date'];
filters = ['status'];
}
@Component({
templateUrl: '../base-view/templates/new.template.html'
})
export class ContractsNewComponent extends BaseContractsComponent {
@Input() value: {} = {};
}
@Component({
template:`
<ng-container *ngIf="this.resourceReadyToPrint; else previewLink">
<label i18n>Download Link:</label>
<div class="input-group mb-12">
<span class="input-group-text"><a href="{{ this.contractPrintLink! }}" target="_blank">{{ this.contractPrintLink! }}</a></span>
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPrintLink!"><i-bs name="text-paragraph"/></button>
</div>
</ng-container>
<ng-template #previewLink>
<label i18n>Preview Link:</label>
<div class="input-group mb-12">
<span class="input-group-text"><a href="{{ this.contractPreviewLink! }}" target="_blank">{{ this.contractPreviewLink! }}</a></span>
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPreviewLink!"><i-bs name="text-paragraph"/></button>
</div>
</ng-template>
<base-card
[resource_id]="this.resource_id"
[resource]="this.resource"
[schema]="this.schema"
(resourceReceived)="this.onResourceReceived($event)">
</base-card>
`,
})
export class ContractsCardComponent extends BaseContractsComponent{
resource_id: string | null = null;
resourceReadyToPrint = false;
contractPrintLink: string | null = null;
contractPreviewLink: string | null = null;
constructor(
private route: ActivatedRoute
) {
super()
}
ngOnInit(): void {
if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id')
})
}
}
onResourceReceived(model: any): void {
this.resourceReadyToPrint = model.status != "published";
this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}`
this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${this.resource_id}`
}
}
@Component({
template: `
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-1">
<ngb-panel>
<ng-template ngbPanelTitle>
<span i18n>Preview</span>
</ng-template>
<ng-template ngbPanelContent>
<iframe width="100%"
[src]="getPreview()"
onload='javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";o.style.width=o.contentWindow.document.body.scrollWidth+"px";}(this));' style="height:200px;width:100%;border:none;overflow:hidden;"></iframe>
</ng-template>
</ngb-panel>
<ngb-panel>
<ng-template ngbPanelTitle>
<span i18n>Signature</span>
</ng-template>
<ng-template ngbPanelContent>
<ng-container *ngIf="this.affixed"><ng-container i18n>This Contract has already been signed by</ng-container> {{ this.signatory }}</ng-container>
<div class="row" *ngIf="!this.affixed">
<signature-drawer class="col-7"
(signatureDrawn$)="postSignature($event)"></signature-drawer>
<div class="col-5" i18n>
<p>Cette page est à la destination exclusive de <strong>{{ this.signatory }}</strong></p>
<p>Si vous n'êtes <strong>pas</strong> {{ this.signatory }}, veuillez <strong>fermer cette page immédiatement</strong> et surpprimer tous les liens en votre possession menant vers celle-ci.</p>
<p>En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour <strong>usurpation d'identité</strong> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</p>
<p>Le cabinet Cooper, Hillman & Toshi LLC</p>
</div>
</div>
</ng-template>
</ngb-panel>
</ngb-accordion>
`
})
export class ContractsSignatureComponent implements OnInit {
signature_id: string | null = null;
signature: any = {}
signatory = ""
affixed = false;
constructor(
private route: ActivatedRoute,
private sanitizer: DomSanitizer,
private crudService: ImageUploaderCrudService,
) {}
ngOnInit() {
this.route.paramMap.subscribe((params: ParamMap) => {
this.signature_id = params.get('id');
this.crudService.get('contract/signature', this.signature_id!).subscribe( (response:any) => {
this.signature = response;
this.affixed = this.signature.signature_affixed;
if (this.signature.representative) {
this.signatory = this.signature.representative.entity_data.firstname + " " + this.signature.representative.entity_data.lastname;
} else {
this.signatory = this.signature.entity.entity_data.firstname + " " + this.signature.entity.entity_data.lastname;
}
})
})
}
getPreview() {
return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/signature/" + this.signature_id);
}
postSignature(image: string) {
this.crudService.upload('contract/signature', this.signature_id!, image).subscribe((v: any) => {
if (v) {
this.affixed = true;
}
})
}
}

View File

@@ -3,11 +3,16 @@ import { NgModule } from '@angular/core';
import { BaseViewModule } from "../base-view/base-view.module"; import { BaseViewModule } from "../base-view/base-view.module";
import { ContractsRoutingModule } from './contracts-routing.module'; import { ContractsRoutingModule } from './contracts-routing.module';
import { DraftCardComponent, DraftListComponent, DraftNewComponent, DraftNewFormComponent } from "./drafts.component";
import { FormlyModule } from "@ngx-formly/core"; import { FormlyModule } from "@ngx-formly/core";
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap"; import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type"; import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
import { CrudService } from "@common/crud/crud.service"; import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service";
import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent } from "../contracts/contracts.component";
import { AlphaRangeComponent, BlackBlueRangeComponent, SignatureDrawerComponent } from "./signature-drawer/signature-drawer.component";
import { ClipboardModule } from "@angular/cdk/clipboard";
@NgModule({ @NgModule({
@@ -15,20 +20,27 @@ import { CrudService } from "@common/crud/crud.service";
CommonModule, CommonModule,
BaseViewModule, BaseViewModule,
ContractsRoutingModule, ContractsRoutingModule,
NgbAccordionModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
FormlyModule.forRoot({ FormlyModule.forRoot({
types: [ types: [
{ name: 'foreign-key', component: ForeignkeyTypeComponent } { name: 'foreign-key', component: ForeignkeyTypeComponent }
] ]
}), }),
FormlyBootstrapModule, FormlyBootstrapModule,
ClipboardModule,
], ],
declarations: [ declarations: [
DraftListComponent, ContractsListComponent,
DraftNewComponent, ContractsNewComponent,
DraftCardComponent, ContractsCardComponent,
DraftNewFormComponent ContractsSignatureComponent,
SignatureDrawerComponent,
BlackBlueRangeComponent,
AlphaRangeComponent
], ],
providers: [CrudService] providers: [CrudService, ImageUploaderCrudService]
}) })
export class ContractsModule { export class ContractsModule {
} }

View File

@@ -1,92 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormlyFieldConfig } from "@ngx-formly/core";
import { FormGroup} from "@angular/forms";
import { CrudFormlyJsonschemaService } from "@common/crud/crud-formly-jsonschema.service";
import { CrudService } from "@common/crud/crud.service";
export class BaseEntitiesComponent {
protected resource: string = "contract/draft";
protected schema: string = "ContractDraft";
}
@Component({
templateUrl: '../base-view/templates/list.template.html'
})
export class DraftListComponent extends BaseEntitiesComponent {
columns = [];
}
@Component({
selector: 'draft-new-form',
template: `<base-new [resource]="this.resource" [schema]="this.schema" [model]="this.value"></base-new>`
})
export class DraftNewFormComponent extends BaseEntitiesComponent {
@Input() value: {} = {};
}
@Component({
selector: 'draft-new',
template: `
<formly-form [fields]="temaplateFormfields" [form]="temaplateForm"></formly-form>
<draft-new-form [value]="this.templateModel"></draft-new-form>
`
})
export class DraftNewComponent extends BaseEntitiesComponent implements OnInit {
templateModel: {} = {};
temaplateFormfields: FormlyFieldConfig[] = [];
temaplateForm: FormGroup = new FormGroup({});
fieldJson = {
type: "object",
properties: {
template_id: {
type: "string",
title: "Find a template",
foreignKey: {
reference: {
resource: "template/contract",
schema: "ContractTemplate"
}
}
}
},
}
constructor(private formlyJsonschema: CrudFormlyJsonschemaService,
private crudService: CrudService
) {
super();
}
ngOnInit() {
// @ts-ignore
this.temaplateFormfields = [this.formlyJsonschema.toFieldConfig(this.fieldJson)];
this.temaplateForm.valueChanges.subscribe((values) => {
if (values.template_id !== undefined) {
this.crudService.get("template/contract", values.template_id).subscribe((templateModel) => {
delete templateModel._id;
delete templateModel.created_at;
delete templateModel.updated_at;
delete templateModel.label;
const provisions = [];
for (const p of templateModel.provisions) {
provisions.push({
provision: { type: "template", provision_template_id: p.provision_template_id}
})
}
templateModel.provisions = provisions;
this.templateModel = templateModel;
});
} else {
this.templateModel = {};
}
})
}
}
@Component({
templateUrl: '../base-view/templates/card.template.html'
})
export class DraftCardComponent extends BaseEntitiesComponent{
}

View File

@@ -0,0 +1,22 @@
canvas {
border: solid black 1px;
}
.btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
cursor: inherit;
display: block;
}

View File

@@ -0,0 +1,48 @@
<div class="row align-items-start">
<canvas id="signatureCanvas" class="col-9" width="320" height="320"></canvas>
<div class="col-3" style="width: 90px">
<div class="card">
<span class="btn btn-light btn-file">
<i-bs name="image-fill"></i-bs><input #imageInput type="file" accept="image/png, image/gif, image/jpeg, image/bmp" (change)="addImage($event)">
</span>
<div #collapseImage="ngbCollapse" [(ngbCollapse)]="!isEditImage">
<color-range [value]="this.currentColor" (change)="updateColor($event)"></color-range>
<alpha-range (change)="updateAlpha($event)"></alpha-range>
</div>
</div>
<div class="card">
<button
type="button"
class="btn btn-light"
(click)="this.toggleDrawing();"
[ngClass]="{active: this.isDrawing}"
><i-bs name="pencil-fill"></i-bs></button>
<div #collapseDrawing="ngbCollapse" [(ngbCollapse)]="!isDrawing">
<color-range [value]="this.currentColor" (change)="this.updateColor($event)"></color-range>
<label for="thickRange" class="form-label">Thickness</label>
<input type="range" class="form-range" #thickRange [value]="this.currentThickness" max="100" (input)="updateThickness(thickRange.value)">
<input class="form-control" type="text" #thickInput [value]="this.currentThickness" (change)="updateThickness(thickInput.value)">
</div>
</div>
<div class="card">
<button
type="button"
class="btn btn-danger"
(click)="delete()"
><i-bs name="eraser-fill"></i-bs></button>
</div>
</div>
</div>
<button
type="button"
class="btn btn-primary"
(click)="sign()"
>Sign!</button>
<button
type="button"
class="btn btn-danger"
(click)="clear()"
>Clear</button>

View File

@@ -0,0 +1,244 @@
import {Component, Output, EventEmitter, OnInit, Input} from "@angular/core";
import { fabric } from 'fabric';
@Component({
selector: 'signature-drawer',
templateUrl: './signature-drawer.component.html',
styleUrls:['./signature-drawer.component.css']
})
export class SignatureDrawerComponent implements OnInit
{
@Output() signatureDrawn$ = new EventEmitter<string>()
size = 320;
canvas: any;
isEditImage = false;
isDrawing = false;
canDelete = false;
currentColor = "rgba(0,0,0,1)";
currentAlpha = 1;
currentThickness = 4;
elements : any[] = [];
ngOnInit() {
this.canvas = new fabric.Canvas('signatureCanvas');
let self = this;
this.canvas.on({
'selection:updated': function() {self.handleElement()},
'selection:created': function() {self.handleElement()}
});
const image = localStorage.getItem('signature_image');
if (image) {
fabric.Image.fromURL(image , function(img: any) {
self.canvas.add(img);
self.canvas.renderAll();
})
}
}
toggleDrawing() {
if (this.isDrawing) {
this.isDrawing = false;
this.canvas.isDrawingMode = false;
} else {
this.canvas.discardActiveObject().renderAll();
this.isEditImage = false;
this.isDrawing = true;
this.canvas.isDrawingMode = true;
let brush = this.canvas.freeDrawingBrush;
brush.color = this.currentColor;
brush.width = this.currentThickness;
}
//this.canvas.freeDrawingBrush.width
//this.canvas.freeDrawingBrush.shadow.blur = parseInt(this.value, 10)
}
handleElement(){
let selectedObjects = this.canvas.getActiveObject();
if (selectedObjects.hasOwnProperty("filters")) {
this.isEditImage = true;
} else {
this.isEditImage = false;
}
}
updateColor(value: string) {
if (typeof value == "object") {
return;
}
this.currentColor = value;
for (let o of this.getSelectedObjects()) {
this.updateColorFilter(o);
}
let brush = this.canvas.freeDrawingBrush;
brush.color = this.currentColor;
this.canvas.renderAll();
}
updateAlpha(value: number) {
if (typeof value == "object") {
return;
}
this.currentAlpha = value;
for (let o of this.getSelectedObjects()) {
o.opacity = this.currentAlpha;
}
this.canvas.renderAll();
}
updateThickness(value: string) {
this.currentThickness = +value
let brush = this.canvas.freeDrawingBrush;
brush.width = this.currentThickness;
}
getSelectedObjects(): any[] {
let obj = this.canvas.getActiveObject();
if (obj === undefined || obj === null) {
return []
} else if (obj.hasOwnProperty("_objects")) {
return obj._objects;
} else {
return [obj];
}
}
updateColorFilter(o: any) {
if (o.hasOwnProperty("filters")) {
for (let f of o.filters) {
if (f.type == "BlendColor") {
f.color = this.currentColor;
}
}
o.applyFilters()
}
}
addImage(event: any) {
let file = event.target.files[0]
let url = URL.createObjectURL(file);
let self = this;
fabric.Image.fromURL(url, function(img: any) {
let scale;
if (img.width > img.height) {
img.scaleToWidth(self.size, true);
scale = self.size / img.width;
} else {
img.scaleToHeight(self.size, true);
scale = self.size / img.height;
}
let resizeFilter = new fabric.Image.filters.Resize(({
resizeType: 'sliceHack',
scaleX: scale,
scaleY: scale
}));
//img.filters.push(resizeFilter);
img.filters.push(new fabric.Image.filters.Grayscale());
// @ts-ignore
img.filters.push(new fabric.Image.filters.RemoveColor())
img.filters.push(new fabric.Image.filters.BlendColor({
color: self.currentColor,
mode: 'lighten',
}));
img.applyFilters();
self.canvas.add(img);
self.canvas.renderAll();
self.elements.push(img);
event.target.value = "";
});
}
delete() {
this.canvas.remove(this.canvas.getActiveObject());
}
clear() {
this.canvas.clear();
}
sign() {
const image = this.canvas.toDataURL({
format: 'png',
quality: 0.8
});
localStorage.setItem('signature_image', image);
this.signatureDrawn$.next(image)
}
}
@Component({
selector: 'color-range',
template: `
<div>
<label for="colorRange" class="form-label">Color</label>
<input type="range" class="form-range" #colorRange [value]="this.currentColorRatio" max="255" (input)="this.updateColor(colorRange.value)">
<div class="input-group">
<input class="form-control" type="text" #colorInput [value]="this.currentColorRatio" (change)="this.updateColor(colorInput.value)">
<span class="input-group-text" #colorShow [style.background-color]="this.value" style="width: 40px">&nbsp;</span>
</div>
</div>
`
})
export class BlackBlueRangeComponent implements OnInit
{
@Output() change = new EventEmitter<string>();
currentColorRatio = 255;
@Input() value = `rgba(0,0,${this.currentColorRatio},1)`;
ngOnInit() {
let rgbArr = this.value.substring(4, this.value.length-1).replace(/ /g, '').split(',');
this.currentColorRatio = +rgbArr[2];
}
updateColor(value: string) {
this.currentColorRatio = +value;
this.value = `rgb(0,0,${this.currentColorRatio})`;
this.change.emit(this.value)
}
}
@Component({
selector: 'alpha-range',
template: `
<label for="alphaRange" class="form-label">Opacity</label>
<input type="range" class="form-range" #alphaRange [value]="this.currentAlphaRatio" max="100" (input)="this.updateAlpha(alphaRange.value)">
<input class="form-control" type="text" #alphaInput [value]="this.currentAlphaRatio" (change)="this.updateAlpha(alphaInput.value)">
`
})
export class AlphaRangeComponent implements OnInit
{
@Output() change = new EventEmitter<number>();
currentAlphaRatio = 100;
@Input() value = this.currentAlphaRatio / 100;
ngOnInit() {
this.currentAlphaRatio = this.value * 100;
}
updateAlpha(value: string) {
this.currentAlphaRatio = +value;
this.value = this.currentAlphaRatio / 100;
this.change.emit(this.value)
}
}

View File

@@ -11,6 +11,7 @@ export class BaseEntitiesComponent {
}) })
export class EntityListComponent extends BaseEntitiesComponent { export class EntityListComponent extends BaseEntitiesComponent {
columns = ['label', 'address', 'entity_data.type'] columns = ['label', 'address', 'entity_data.type']
filters = [];
} }
@Component({ @Component({

View File

@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { EntitiesRoutingModule } from './entities-routing.module'; import { EntitiesRoutingModule } from './entities-routing.module';
import { EntityCardComponent, EntityListComponent, EntityNewComponent} from "./entities.component";
import { BaseViewModule } from "../base-view/base-view.module"; import { BaseViewModule } from "../base-view/base-view.module";
import { EntityCardComponent, EntityListComponent, EntityNewComponent} from "./entities.component";

View File

@@ -7,20 +7,21 @@ export class BaseContractTemplateComponent {
} }
@Component({ @Component({
templateUrl: '../base-view/templates/list.template.html' templateUrl: '../base-view/templates/list.template.html'
}) })
export class ContractTemplateListComponent extends BaseContractTemplateComponent { export class ContractTemplateListComponent extends BaseContractTemplateComponent {
columns = []; columns = ['name', 'title', 'parties.items.part'];
filters = [];
} }
@Component({ @Component({
templateUrl: '../base-view/templates/new.template.html' templateUrl: '../base-view/templates/new.template.html'
}) })
export class ContractTemplateNewComponent extends BaseContractTemplateComponent { export class ContractTemplateNewComponent extends BaseContractTemplateComponent {
} }
@Component({ @Component({
templateUrl: '../base-view/templates/card.template.html' templateUrl: '../base-view/templates/card.template.html'
}) })
export class ContractTemplateCardComponent extends BaseContractTemplateComponent { export class ContractTemplateCardComponent extends BaseContractTemplateComponent {
} }

View File

@@ -7,20 +7,21 @@ export class BaseProvisionTemplateComponent {
} }
@Component({ @Component({
templateUrl: '../base-view/templates/list.template.html' templateUrl: '../base-view/templates/list.template.html'
}) })
export class ProvisionTemplateListComponent extends BaseProvisionTemplateComponent{ export class ProvisionTemplateListComponent extends BaseProvisionTemplateComponent{
columns = []; columns = ['name', 'title', 'body'];
filters = [];
} }
@Component({ @Component({
templateUrl: '../base-view/templates/new.template.html' templateUrl: '../base-view/templates/new.template.html'
}) })
export class ProvisionTemplateNewComponent extends BaseProvisionTemplateComponent { export class ProvisionTemplateNewComponent extends BaseProvisionTemplateComponent {
} }
@Component({ @Component({
templateUrl: '../base-view/templates/card.template.html' templateUrl: '../base-view/templates/card.template.html'
}) })
export class ProvisionTemplateCardComponent extends BaseProvisionTemplateComponent { export class ProvisionTemplateCardComponent extends BaseProvisionTemplateComponent {
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1,36 +1,32 @@
<div> <div>
<form cForm [formGroup]="form" (ngSubmit)="onSubmit(model)"> <form cForm [formGroup]="form" (ngSubmit)="onSubmit(model)">
<span class="col col-form-label" *ngIf="formLoading$ || modelLoading$ | async">Loading...</span> <span class="col col-form-label" i18n *ngIf="formLoading$ || modelLoading$ | async">Loading...</span>
<formly-form [form]="form" [fields]="fields" [model]="model"></formly-form> <formly-form [form]="form" [fields]="fields" [model]="model"></formly-form>
<div class="d-grid gap-2 d-md-flex"> <div class="d-grid gap-2 d-md-flex">
<button class="btn btn-success btn-lg" type="submit" <button class="btn btn-success btn-lg" type="submit"
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)"> [disabled]="!form.valid && (formLoading$ || modelLoading$ | async)">
{{ this.isCreateForm() ? "Create" : "Update" }} {{ submitText }}
</button> </button>
<button class="btn btn-primary btn-lg" type="button" *ngIf="!this.isCreateForm()" <button class="btn btn-primary btn-lg" i18n type="button" *ngIf="!this.isCreateForm()"
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)" [disabled]="!form.valid && (formLoading$ || modelLoading$ | async)"
(click)="open(duplicationModal)"> (click)="open(duplicationModal)">Duplicate</button>
Duplicate <button class="btn btn-danger btn-lg" i18n type="button" *ngIf="!this.isCreateForm()"
</button>
<button class="btn btn-danger btn-lg" type="button" *ngIf="!this.isCreateForm()"
[disabled]="formLoading$ || modelLoading$ | async" [disabled]="formLoading$ || modelLoading$ | async"
(click)="open(confirmDeleteModal)"> (click)="open(confirmDeleteModal)">Delete</button>
Delete
</button>
<ng-template #confirmDeleteModal let-modal> <ng-template #confirmDeleteModal let-modal>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">Are you sure you want to delete this {{ this.schema }}?</h4> <h4 class="modal-title" i18n>Are you sure you want to delete this {{ this.schema }}?</h4>
<button type="button" class="btn-close" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button> <button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-light" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')">Cancel</button> <button type="button" class="btn btn-light" i18n i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')">Cancel</button>
<button type="button" class="btn btn-danger" (click)="modal.close('Save click')">Delete</button> <button type="button" class="btn btn-danger" i18n (click)="modal.close('Save click')">Delete</button>
</div> </div>
</ng-template> </ng-template>
<ng-template #duplicationModal let-modal> <ng-template #duplicationModal let-modal>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">Duplicate {{ this.schema }}</h4> <h4 class="modal-title" i18n>Duplicate {{ this.schema }}</h4>
<button type="button" class="btn-close" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button> <button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<crud-card [resource]="this.resource" <crud-card [resource]="this.resource"

View File

@@ -9,15 +9,15 @@ import { CrudFormlyJsonschemaService } from "../crud-formly-jsonschema.service";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
@Component({ @Component({
selector: 'crud-card', selector: 'crud-card',
templateUrl: './card.component.html', templateUrl: './card.component.html',
styleUrls: ['./card.component.css'] styleUrls: ['./card.component.css']
}) })
export class CardComponent implements OnInit { export class CardComponent implements OnInit {
@Input() resource: string | undefined; @Input() resource: string | undefined;
@Input() resource_id: string | null = null; @Input() resource_id: string | null = null;
@Input() schema: string | undefined; @Input() schema: string | undefined;
@Input() is_modal: Boolean = false; @Input() is_modal: Boolean = false;
private _model: {} = {}; private _model: {} = {};
@@ -37,12 +37,12 @@ export class CardComponent implements OnInit {
@Output() resourceUpdated: EventEmitter<string> = new EventEmitter(); @Output() resourceUpdated: EventEmitter<string> = new EventEmitter();
@Output() resourceDeleted: EventEmitter<string> = new EventEmitter(); @Output() resourceDeleted: EventEmitter<string> = new EventEmitter();
@Output() error: EventEmitter<string> = new EventEmitter(); @Output() error: EventEmitter<string> = new EventEmitter();
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
form = new FormGroup({});
fields: FormlyFieldConfig[] = [];
form = new FormGroup({}); schemas = JSON.parse(`{}`);
fields: FormlyFieldConfig[] = [];
schemas = JSON.parse(`{}`);
private _formLoading$ = new BehaviorSubject<boolean>(true); private _formLoading$ = new BehaviorSubject<boolean>(true);
private _modelLoading$ = new BehaviorSubject<boolean>(true); private _modelLoading$ = new BehaviorSubject<boolean>(true);
@@ -55,12 +55,16 @@ export class CardComponent implements OnInit {
return this._modelLoading$.asObservable(); return this._modelLoading$.asObservable();
} }
constructor(private crudService: CrudService, get submitText() {
private formlyJsonschema: CrudFormlyJsonschemaService, return this.isCreateForm() ? $localize`Create` : $localize`Update`
private router: Router, }
private route: ActivatedRoute,
private modalService: NgbModal, constructor(private crudService: CrudService,
) { } private formlyJsonschema: CrudFormlyJsonschemaService,
private router: Router,
private route: ActivatedRoute,
private modalService: NgbModal,
) { }
ngOnInit(): void { ngOnInit(): void {
this._formLoading$.next(true); this._formLoading$.next(true);
@@ -74,6 +78,7 @@ export class CardComponent implements OnInit {
next :(model: any) => { next :(model: any) => {
this.model = model; this.model = model;
this._modelLoading$.next(false); this._modelLoading$.next(false);
this.resourceReceived.emit(model);
}, },
error: (err) => this.error.emit("Error loading the model:" + err) error: (err) => this.error.emit("Error loading the model:" + err)
}); });
@@ -92,27 +97,28 @@ export class CardComponent implements OnInit {
onSubmit(model: any) { onSubmit(model: any) {
this._modelLoading$.next(true); this._modelLoading$.next(true);
if (this.isCreateForm()) { if (this.isCreateForm()) {
this.crudService.create(this.resource!, model).subscribe({ this.crudService.create(this.resource!, model).subscribe({
next: (response: any) => { next: (response: any) => {
this._modelLoading$.next(false); this._modelLoading$.next(false);
if (! this.is_modal) { if (! this.is_modal) {
this.router.navigate([`../${response.id}`], {relativeTo: this.route}); this.router.navigate([`../${response.id}`], {relativeTo: this.route});
} else { } else {
this.resourceCreated.emit(response.id) this.resourceCreated.emit(response.id)
} }
}, },
error: (err) => this.error.emit("Error creating the entity:" + err) error: (err) => this.error.emit("Error creating the entity:" + err)
}); });
} else { } else {
model._id = this.resource_id; model._id = this.resource_id;
this.crudService.update(this.resource!, model).subscribe( { this.crudService.update(this.resource!, model).subscribe( {
next: (model: any) => { next: (model: any) => {
this.model = model; this.resourceUpdated.emit(model._id);
this._modelLoading$.next(false); this.resourceReceived.emit(model);
this.resourceUpdated.emit(model._id) this.model = model;
}, this._modelLoading$.next(false);
error: (err) => this.error.emit("Error updating the entity:" + err) },
}); error: (err) => this.error.emit("Error updating the entity:" + err)
});
} }
} }
@@ -136,9 +142,9 @@ export class CardComponent implements OnInit {
this.modalService.dismissAll(); this.modalService.dismissAll();
} }
isCreateForm() { isCreateForm() {
return this.resource_id === null; return this.resource_id === null;
} }
open(content: any) { open(content: any) {
this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then( this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then(

View File

@@ -43,13 +43,20 @@ export class CrudFormlyJsonschemaOptions implements FormlyJsonschemaOptions {
field.type = "datetime"; field.type = "datetime";
} else if (schema.format === 'date') { } else if (schema.format === 'date') {
field.type = "date"; field.type = "date";
} else if (schema.hasOwnProperty('enum') && schema.enum.length == 1 && schema.enum[0] == schema.default ) { } else if (
(schema.hasOwnProperty('hidden') && schema.hidden)
|| (schema.hasOwnProperty('enum') && schema.enum.length == 1 && schema.enum[0] == schema.default)
) {
field.type = "hidden"; field.type = "hidden";
} else if (schema.type == "array" && schema.format == "dictionary") { } else if (schema.type == "array" && schema.format == "dictionary") {
field.type = "dictionary"; field.type = "dictionary";
} else if (schema.type == "string" && schema.hasOwnProperty('props') } else if (schema.type == "string" && schema.hasOwnProperty('props')
&& schema.props.hasOwnProperty("richtext") && schema.props.richtext) { && schema.props.hasOwnProperty("richtext") && schema.props.richtext) {
field.type = "richtext"; field.type = "richtext";
} else if (schema.type == "string" && schema.format == "signature-link") {
field.type = "signature-link";
} else if (field.type == "enum" && field.props.multiple) {
field.type = 'multicheckbox';
} }
if (schema.hasOwnProperty('props')) { if (schema.hasOwnProperty('props')) {

View File

@@ -6,7 +6,7 @@ import { ListComponent } from "./list/list.component";
const routes: Routes = [ const routes: Routes = [
{ path: '', component: ListComponent }, { path: '', component: ListComponent },
{ path: ':id', component: CardComponent }, { path: ':id', component: CardComponent },
];; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],

View File

@@ -15,7 +15,7 @@ import { ArrayTypeComponent } from "./types/array.type";
import { ObjectTypeComponent } from "./types/object.type"; import { ObjectTypeComponent } from "./types/object.type";
import { DatetimeTypeComponent } from "./types/datetime.type"; import { DatetimeTypeComponent } from "./types/datetime.type";
import { DateTypeComponent } from "./types/date.type"; import { DateTypeComponent } from "./types/date.type";
import { ApiService, CrudService } from "./crud.service"; import { ApiService, CrudService, ImageUploaderCrudService } from "./crud.service";
import { CrudFormlyJsonschemaService } from "./crud-formly-jsonschema.service"; import { CrudFormlyJsonschemaService } from "./crud-formly-jsonschema.service";
import { NgbModule} from "@ng-bootstrap/ng-bootstrap"; import { NgbModule} from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons"; import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
@@ -26,12 +26,16 @@ import { HiddenTypeComponent } from "./types/hidden.type";
import { DictionaryTypeComponent } from "./types/dictionary.type"; import { DictionaryTypeComponent } from "./types/dictionary.type";
import { DictionaryService } from "./types/dictionary.service"; import { DictionaryService } from "./types/dictionary.service";
import { RichtextTypeComponent } from "./types/richtext.type"; import { RichtextTypeComponent } from "./types/richtext.type";
import { SignatureLinkTypeComponent } from "@common/crud/types/signature-link.type";
import { ClipboardModule } from '@angular/cdk/clipboard';
import {FilterListComponent} from "@common/crud/list/filter-list.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
CardComponent, CardComponent,
ListComponent, ListComponent,
FilterListComponent,
ObjectTypeComponent, ObjectTypeComponent,
DatetimeTypeComponent, DatetimeTypeComponent,
DateTypeComponent, DateTypeComponent,
@@ -40,12 +44,14 @@ import { RichtextTypeComponent } from "./types/richtext.type";
ForeignkeyTypeComponent, ForeignkeyTypeComponent,
HiddenTypeComponent, HiddenTypeComponent,
DictionaryTypeComponent, DictionaryTypeComponent,
RichtextTypeComponent RichtextTypeComponent,
SignatureLinkTypeComponent
], ],
providers: [ providers: [
JsonschemasService, JsonschemasService,
ApiService, ApiService,
CrudService, CrudService,
ImageUploaderCrudService,
CrudFormlyJsonschemaService, CrudFormlyJsonschemaService,
DictionaryService DictionaryService
], ],
@@ -68,10 +74,12 @@ import { RichtextTypeComponent } from "./types/richtext.type";
{ name: 'hidden', component: HiddenTypeComponent }, { name: 'hidden', component: HiddenTypeComponent },
{ name: 'dictionary', component: DictionaryTypeComponent }, { name: 'dictionary', component: DictionaryTypeComponent },
{ name: 'richtext', component: RichtextTypeComponent }, { name: 'richtext', component: RichtextTypeComponent },
{ name: 'signature-link', component: SignatureLinkTypeComponent },
] ]
}), }),
FormlyBootstrapModule, FormlyBootstrapModule,
EditorModule EditorModule,
ClipboardModule
], ],
exports: [ exports: [
CardComponent, CardComponent,

View File

@@ -116,3 +116,28 @@ export class CrudService extends ApiService {
); );
} }
} }
@Injectable()
export class ImageUploaderCrudService extends CrudService {
public upload(resource: string, signature_id: string, image: string) {
const formData: FormData = new FormData();
formData.append("signature_file", dataURIToBlob(image), signature_id + ".png");
return this.http.post<{ menu: [{}] }>(
`${this.api_root}/${resource.toLowerCase()}/${signature_id}`,
formData
);
}
}
function dataURIToBlob(dataURI: string) {
const splitDataURI = dataURI.split(',')
const byteString = splitDataURI[0].indexOf('base64') >= 0 ? atob(splitDataURI[1]) : decodeURI(splitDataURI[1])
const mimeString = splitDataURI[0].split(':')[1].split(';')[0]
const ia = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++)
ia[i] = byteString.charCodeAt(i)
return new Blob([ia], { type: mimeString })
}

View File

@@ -30,7 +30,7 @@ export class JsonschemasService {
buildResource(resourceName: string) { buildResource(resourceName: string) {
let resource; let resource;
resource = { ... this.rawSchemas.components.schemas[resourceName]}; resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} }; resource.components = { schemas: {} };
for (let prop_name in resource.properties) { for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name]; let prop = resource.properties[prop_name];
@@ -94,12 +94,34 @@ export class JsonschemasService {
} }
} }
} }
this.changePropertiesOrder(resource);
observer.next(resource); observer.next(resource);
}) })
}) })
} }
changePropertiesOrder(resource: any) {
let created_at;
let updated_at;
let new_properties: any = {};
for (let prop_name in resource.properties) {
if (prop_name == 'created_at') {
created_at = resource.properties[prop_name];
} else if (prop_name == 'updated_at') {
updated_at = resource.properties[prop_name];
} else {
new_properties[prop_name] = resource.properties[prop_name];
}
}
if (created_at) {
new_properties['created_at'] = created_at;
}
if (updated_at) {
new_properties['updated_at'] = updated_at;
}
resource.properties = new_properties
}
private is_object(prop: any) { private is_object(prop: any) {
return prop.hasOwnProperty('properties') return prop.hasOwnProperty('properties')
} }
@@ -140,6 +162,14 @@ export class JsonschemasService {
} }
return false; return false;
} }
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
return true;
}
return false;
}
} }
throw new Error("Jsonschema format not implemented in property finder"); throw new Error("Jsonschema format not implemented in property finder");
return false; return false;
@@ -159,7 +189,15 @@ export class JsonschemasService {
} else if (this.is_union(resource)) { } else if (this.is_union(resource)) {
for (const ref of resource.oneOf!) { for (const ref of resource.oneOf!) {
// @ts-ignore // @ts-ignore
if (this.has_property(ref, property_name)) { if (this.has_descendant(ref, property_name)) {
// @ts-ignore
return this.get_descendant(ref, property_name);
}
}
} else if (this.is_enum(resource)) {
for (const ref of resource.allOf!) {
// @ts-ignore
if (this.has_descendant(ref, property_name)) {
// @ts-ignore // @ts-ignore
return this.get_descendant(ref, property_name); return this.get_descendant(ref, property_name);
} }
@@ -184,6 +222,21 @@ export class JsonschemasService {
path.substring(pointFirstPosition + 1) path.substring(pointFirstPosition + 1)
); );
} }
get_property_by_path(resource: JSONSchema7, path: string): JSONSchema7 {
const pointFirstPosition = path.indexOf('.')
if (pointFirstPosition == -1) {
return this.get_descendant(resource, path);
}
return this.get_property_by_path(
this.get_descendant(
resource,
path.substring(0, pointFirstPosition)
),
path.substring(pointFirstPosition + 1)
);
}
} }
export interface Schema { export interface Schema {

View File

@@ -0,0 +1,99 @@
import {Component, EventEmitter, Input, OnInit, Output} from "@angular/core";
import {JSONSchema7} from "json-schema";
import {JsonschemasService} from "@common/crud/jsonschemas.service"
import {FormGroup} from "@angular/forms";
import {FormlyFieldConfig} from "@ngx-formly/core";
import {CrudFormlyJsonschemaService} from "@common/crud/crud-formly-jsonschema.service";
@Component({
selector: 'crud-list-filter-list',
template: `
<formly-form [form]="form" [fields]="fields" [model]="this.searchTerms" (modelChange)="onModelChange($event)"></formly-form>
`,
})
export class FilterListComponent implements OnInit {
@Input() filters: string[] = [];
@Input() schema = "";
@Input() values = {};
@Output() filterChange: EventEmitter<{[key: string]: any}> = new EventEmitter();
form = new FormGroup({});
fields: FormlyFieldConfig[] = [];
searchTerms: {[key: string]: string | {}} = {}
public fieldJson = {
components: {},
type: "object",
properties: {},
}
constructor(private jsonSchemasService: JsonschemasService,
private formlyJsonschema: CrudFormlyJsonschemaService,) { }
ngOnInit() {
this.jsonSchemasService.getUpdateResource(this.schema!).subscribe({
next: (schema: any) => this.getFilterDefinition(schema),
error: (err) => console.log(err) /*this.error.emit("Error loading the schema:" + err)*/
});
}
getFilterDefinition(schema: JSONSchema7) {
const properties: {[key: string]: JSONSchema7} = {}
for (let filter of this.filters) {
if (this.jsonSchemasService.path_exists(schema, filter)) {
let prop = this.jsonSchemasService.get_property_by_path(schema, filter)
if (prop.hasOwnProperty('allOf')) {
// @ts-ignore
prop = schema.components.schemas[prop.allOf![0]['$ref'].replace('#/components/schemas/', '')];
prop.type = "array";
prop.items = {"type": "string", "enum": prop.enum};
if (filter in this.values) {
this.searchTerms[filter] = {};
// @ts-ignore
for (let val of this.values[filter]) {
// @ts-ignore
this.searchTerms[filter][val] = true;
}
}
} else if(true) {
}
if (prop.hasOwnProperty('readOnly') && prop.readOnly) {
prop.readOnly = false
}
properties[filter] = prop;
}
}
// @ts-ignore
this.fieldJson.components = schema.components;
this.fieldJson.properties = properties;
// @ts-ignore
this.fields = [this.formlyJsonschema.toFieldConfig(this.fieldJson)]
}
onModelChange(event: {[key: string]: any}) {
for (let p_name in event) {
// @ts-ignore
let p = this.fieldJson.properties[p_name]
if (p.type == "array" && !Array.isArray(p.items)) {
let value = []
for (let key in event[p_name]) {
if (event[p_name][key]) {
value.push(key)
}
}
if (value.length == 0 || value.length == p.items.enum.length) {
delete event[p_name];
} else {
event[p_name] = value;
}
}
}
this.filterChange.next(event);
}
}

View File

@@ -0,0 +1,3 @@
.table-row-link {
cursor: pointer;
}

View File

@@ -1,10 +1,9 @@
<form> <form>
<button class="btn btn-success btn-lg float-end" type="button" i18n (click)="onCreate()">
<button class="btn btn-success btn-lg float-end" type="button" (click)="onCreate()">
Create {{ this.schema }} Create {{ this.schema }}
</button> </button>
<div class="mb-3 row"> <div class="mb-3 row">
<label for="table-complete-search" class="col-xs-3 col-sm-auto col-form-label">Full text search:</label> <label for="table-complete-search" i18n class="col-xs-3 col-sm-auto col-form-label">Full text search:</label>
<div class="col-xs-3 col-sm-auto"> <div class="col-xs-3 col-sm-auto">
<input <input
id="table-complete-search" id="table-complete-search"
@@ -14,35 +13,45 @@
[(ngModel)]="searchTerm" [(ngModel)]="searchTerm"
/> />
</div> </div>
<span class="col col-form-label" *ngIf="loading$ | async">Loading...</span> <div class="col-xs-3 col-sm-auto">
<crud-list-filter-list
[filters]="this.filters"
[schema]="this.schema!"
[values]="this.searchFilters"
(filterChange)="onFilterChange($event)"
></crud-list-filter-list>
</div>
</div> </div>
<div class="table-responsive-md"> <div class="table-responsive-md">
<table class="table table-striped"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col }}</th> <th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col.title }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let row of listData$ | async" (click)="onSelect(row._id)"> <tr *ngIf="loading$ | async">
<td *ngFor="let col of this.displayedColumns"> <td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td>
<ngb-highlight [result]="getColumnValue(row,col)" [term]="searchTerm"></ngb-highlight> </tr>
<tr *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link">
<td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;">
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex justify-content-between p-2"> <div class="d-flex justify-content-between p-2" *ngIf="! (loading$ | async)" >
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize"> <ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize">
</ngb-pagination> </ngb-pagination>
<select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="pageSize"> <select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="pageSize">
<option [ngValue]="10">10 items per page</option> <option i18n [ngValue]="10">10 items per page</option>
<option [ngValue]="15">15 items per page</option> <option i18n [ngValue]="15">15 items per page</option>
<option [ngValue]="25">25 items per page</option> <option i18n [ngValue]="25">25 items per page</option>
<option [ngValue]="50">50 items per page</option> <option i18n [ngValue]="50">50 items per page</option>
<option [ngValue]="100">100 items per page</option> <option i18n [ngValue]="100">100 items per page</option>
</select> </select>
</div> </div>
</form> </form>

View File

@@ -13,6 +13,12 @@ interface State {
searchTerm: string; searchTerm: string;
sortColumn: SortColumn; sortColumn: SortColumn;
sortDirection: SortDirection; sortDirection: SortDirection;
searchFilters: {[key: string]: any}
}
interface Column {
path: string,
title: string
} }
@Component({ @Component({
@@ -23,6 +29,7 @@ interface State {
export class ListComponent implements OnInit { export class ListComponent implements OnInit {
@Input() resource: string = ""; @Input() resource: string = "";
@Input() columns: string[] = []; @Input() columns: string[] = [];
@Input() filters: string[] = [];
@Input() schema: string | undefined; @Input() schema: string | undefined;
@Output() error: EventEmitter<string> = new EventEmitter(); @Output() error: EventEmitter<string> = new EventEmitter();
@@ -30,10 +37,9 @@ export class ListComponent implements OnInit {
@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader> = new QueryList<NgbdSortableHeader>(); @ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader> = new QueryList<NgbdSortableHeader>();
public displayedColumns: string[] = []; public displayedColumns: Column[] = [];
private _loading$ = new BehaviorSubject<boolean>(true); private _loading$ = new BehaviorSubject<boolean>(true);
//private _search$ = new Subject<void>();
private _listData$ = new BehaviorSubject<any[]>([]); private _listData$ = new BehaviorSubject<any[]>([]);
private _total$ = new BehaviorSubject<number>(0); private _total$ = new BehaviorSubject<number>(0);
@@ -43,6 +49,7 @@ export class ListComponent implements OnInit {
searchTerm: '', searchTerm: '',
sortColumn: '_id', sortColumn: '_id',
sortDirection: 'asc', sortDirection: 'asc',
searchFilters: {}
}; };
constructor(private service: CrudService, constructor(private service: CrudService,
@@ -56,37 +63,62 @@ export class ListComponent implements OnInit {
next: (schema: any) => this.getColumnDefinition(schema), next: (schema: any) => this.getColumnDefinition(schema),
error: (err) => this.error.emit("Error loading the schema:" + err) error: (err) => this.error.emit("Error loading the schema:" + err)
}); });
this._search(); this.route.queryParams.subscribe(params => {
let parsedParams = {...params};
if (parsedParams.hasOwnProperty('searchFilters')) {
parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']);
}
this._set(parsedParams)
});
} }
getColumnDefinition(schema: JSONSchema7) { getColumnDefinition(schema: JSONSchema7) {
for (let column of this.columns) { for (let column of this.columns) {
if (this.jsonSchemasService.path_exists(schema, column)) { if (this.jsonSchemasService.path_exists(schema, column)) {
this.displayedColumns.push(column); this.displayedColumns.push({
path: column,
title: this.jsonSchemasService.get_property_by_path(schema, column).title!
});
} }
} }
if (this.displayedColumns.length == 0) { if (this.displayedColumns.length == 0) {
for (let param_name in schema.properties) { for (let param_name in schema.properties) {
if (param_name != "_id") { if (param_name != "_id") {
this.displayedColumns.push(param_name); this.displayedColumns.push({
path: param_name,
title: this.jsonSchemasService.get_property_by_path(schema, param_name).title!
});
} }
} }
} }
} }
getColumnValue(row: any, col: string) { getColumnValue(row: any, col: string): string {
let parent = row; let parent = row;
for (const key of col.split('.')) { for (const key of col.split('.')) {
parent = parent[key]; if (key == 'items' && Array.isArray(parent)) {
let path_parts = col.split(/items\.(.*)/s);
let subkey = path_parts[1]
return parent.map((v: any) => this.getColumnValue(v, subkey)).join(', ');
} else {
parent = parent[key];
}
} }
return parent; return parent.replace(/<[^>]*>/g, '');
} }
private _search() { private _search() {
this._loading$.next(true); this._loading$.next(true);
let sortBy = new SortBy(this.sortColumn, this.sortDirection) let sortBy = new SortBy(this.sortColumn, this.sortDirection)
let filters = this.searchTerm ? [new Filters('fulltext', 'eq', this.searchTerm)] : []; let filters = this.searchTerm ? [new Filters('fulltext', 'eq', this.searchTerm)] : [];
for (let f in this.searchFilters) {
if (Array.isArray(this.searchFilters[f])) {
filters.push(new Filters(f, 'in', this.searchFilters[f]))
} else {
filters.push(new Filters(f, 'eq', this.searchFilters[f]))
}
}
this.service.getList(this.resource, this.page, this.pageSize, [sortBy], filters).subscribe({ this.service.getList(this.resource, this.page, this.pageSize, [sortBy], filters).subscribe({
next: (data: any) => { next: (data: any) => {
@@ -101,6 +133,10 @@ export class ListComponent implements OnInit {
}); });
} }
onFilterChange(event: any) {
this.searchFilters = event;
}
onSort({ column, direction }: any) { onSort({ column, direction }: any) {
// resetting other headers // resetting other headers
this.headers.forEach((header) => { this.headers.forEach((header) => {
@@ -113,10 +149,15 @@ export class ListComponent implements OnInit {
this.sortDirection = direction; this.sortDirection = direction;
} }
onSelect(id: string) { onRowClick(id: string) {
this.router.navigate([`../${id}`], {relativeTo: this.route}); this.router.navigate([`../${id}`], {relativeTo: this.route});
} }
onRowMiddleClick(id: string) {
let newUrl = window.location.href.replace('list', id).split('?')[0]
window.open(newUrl, '_blank');
}
onCreate() { onCreate() {
this.router.navigate([`../new`], {relativeTo: this.route}); this.router.navigate([`../new`], {relativeTo: this.route});
} }
@@ -145,25 +186,44 @@ export class ListComponent implements OnInit {
get searchTerm() { get searchTerm() {
return this._state.searchTerm; return this._state.searchTerm;
} }
get searchFilters() {
set page(page: number) { return this._state.searchFilters;
this._set({ page });
}
set pageSize(pageSize: number) {
this._set({ pageSize });
}
set searchTerm(searchTerm: string) {
this._set({ searchTerm });
}
set sortColumn(sortColumn: SortColumn) {
this._set({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this._set({ sortDirection });
} }
private _set(patch: Partial<State>) { set page(page: number) {
Object.assign(this._state, patch); this.updateState({ page });
this._search(); }
}
set pageSize(pageSize: number) {
this.updateState({ pageSize });
}
set searchTerm(searchTerm: string) {
this.updateState({ searchTerm });
}
set searchFilters(searchFilters: {[key: string]: any}) {
this.updateState({ searchFilters: JSON.stringify(searchFilters) });
}
set sortColumn(sortColumn: SortColumn) {
this.updateState({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this.updateState({ sortDirection });
}
private updateState(patch: any) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: patch ,
queryParamsHandling: 'merge'
});
}
private _set(patch: Partial<State>) {
Object.assign(this._state, patch);
this._search();
}
} }

View File

@@ -17,7 +17,7 @@ import { FieldArrayType } from '@ngx-formly/core';
<div *ngIf="props['numbered']" class="float-start"> <div *ngIf="props['numbered']" class="float-start">
<strong>{{ i + 1 }}</strong> <strong>{{ i + 1 }}</strong>
</div> </div>
<div class="btn-group float-end"> <div *ngIf="! this.field.props.readonly" class="btn-group float-end">
<button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button> <button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button>
<button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button> <button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button>
<button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button> <button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button>
@@ -29,7 +29,7 @@ import { FieldArrayType } from '@ngx-formly/core';
</div> </div>
</div> </div>
</div> </div>
<button class="btn btn-success col-sm-12" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button> <button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
</div> </div>
`, `,
}) })

View File

@@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import { formatDate } from "@angular/common"; import { formatDate } from "@angular/common";
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { FieldType, FieldTypeConfig } from '@ngx-formly/core'; import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
@@ -15,12 +14,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors"> <div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
<formly-validation-message [field]="field"></formly-validation-message> <formly-validation-message [field]="field"></formly-validation-message>
</div> </div>
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
/>
<div class="input-group" *ngIf="! this.field.props.readonly"> <div class="input-group" *ngIf="! this.field.props.readonly">
<input type="hidden" <button class="btn btn-outline-secondary" (click)="d.toggle()" type="button"><i-bs name="calendar-date-fill"></i-bs></button>
[formControl]="formControl"
[formlyAttributes]="field"
[class.is-invalid]="showError"
/>
<input <input
class="form-control" class="form-control"
placeholder="yyyy-mm-dd" placeholder="yyyy-mm-dd"
@@ -29,33 +28,40 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
(ngModelChange)="changeDatetime($event)" (ngModelChange)="changeDatetime($event)"
ngbDatepicker ngbDatepicker
#d="ngbDatepicker" #d="ngbDatepicker"
[class.is-invalid]="showError"
/> />
<button class="btn btn-outline-secondary" (click)="d.toggle()" type="button"><i-bs name="calendar-date-fill"></i-bs></button>
</div> </div>
<div class="input-group" *ngIf="this.field.props.readonly"> <div class="input-group" *ngIf="this.field.props.readonly">
<input class="form-control" value="{{ this.datetime.toLocaleString() }}" disabled=""/> <input class="form-control" value="{{ this.datetime ? this.datetime.toLocaleString() : '' }}" disabled=""/>
</div> </div>
`, `,
}) })
export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnInit export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
{ {
public date : NgbDateStruct; public date : NgbDateStruct | null = null;
public datetime : Date = new Date(); public datetime : Date | null = null;
constructor() { constructor() {
super(); super();
this.date = this.getDateStruct(new Date());
} }
ngOnInit() { ngOnInit() {
if (this.formControl.value === undefined) { if (this.formControl.value === undefined) {
this.changeDatetime({}); this.changeDatetime({});
} else {
this.datetime = new Date(this.formControl.value);
this.date = this.getDateStruct(this.datetime);
} }
this.formControl.valueChanges.subscribe(value => { this.formControl.valueChanges.subscribe(value => {
this.datetime = new Date(value) if (value) {
this.date = this.getDateStruct(this.datetime); this.datetime = new Date(value);
this.date = this.getDateStruct(this.datetime);
} else {
this.datetime = null;
this.date = null;
}
}) })
} }
@@ -68,12 +74,21 @@ export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnI
} }
changeDatetime(event: any) { changeDatetime(event: any) {
this.datetime.setFullYear(this.date.year) if (this.date) {
this.datetime.setMonth(this.date.month - 1) if (!this.datetime) {
this.datetime.setDate(this.date.day) this.datetime = new Date();
}
this.datetime.setFullYear(this.date.year)
this.datetime.setMonth(this.date.month - 1)
this.datetime.setDate(this.date.day)
this.formControl.setValue( this.formControl.setValue(
formatDate(this.datetime, 'YYYY-MM-dd', 'EN_US', 'CET') formatDate(this.datetime, 'YYYY-MM-dd', 'EN_US', 'CET')
) )
} else {
this.datetime = null;
this.date = null;
this.formControl.setValue('')
}
} }
} }

View File

@@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import { formatDate } from "@angular/common"; import { formatDate } from "@angular/common";
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
import { FieldType, FieldTypeConfig } from '@ngx-formly/core'; import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
@@ -12,12 +11,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
class="form-label">{{ props.label }} class="form-label">{{ props.label }}
<span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span> <span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span>
</label> </label>
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
/>
<div class="input-group" *ngIf="! this.field.props.readonly"> <div class="input-group" *ngIf="! this.field.props.readonly">
<input type="hidden" <button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
[formControl]="formControl"
[formlyAttributes]="field"
[class.is-invalid]="showError"
/>
<input <input
class="form-control" class="form-control"
placeholder="yyyy-mm-dd" placeholder="yyyy-mm-dd"
@@ -26,8 +25,8 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
(ngModelChange)="changeDatetime($event)" (ngModelChange)="changeDatetime($event)"
ngbDatepicker ngbDatepicker
#d="ngbDatepicker" #d="ngbDatepicker"
[class.is-invalid]="showError"
/> />
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
<ngb-timepicker <ngb-timepicker
(ngModelChange)="changeDatetime($event)" (ngModelChange)="changeDatetime($event)"
[(ngModel)]="time" [(ngModel)]="time"
@@ -35,15 +34,15 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
</ngb-timepicker> </ngb-timepicker>
</div> </div>
<div class="input-group" *ngIf="this.field.props.readonly"> <div class="input-group" *ngIf="this.field.props.readonly">
<input class="form-control" value="{{ this.datetime.toLocaleString() }}" disabled=""/> <input class="form-control" value="{{ this.datetime ? this.datetime.toLocaleString() : '' }}" disabled=""/>
</div> </div>
`, `,
}) })
export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements OnInit export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
{ {
public time : NgbTimeStruct; public time : NgbTimeStruct | null = null;
public date : NgbDateStruct; public date : NgbDateStruct | null = null;
public datetime : Date = new Date() public datetime : Date | null = null;
constructor() { constructor() {
@@ -55,12 +54,21 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
ngOnInit() { ngOnInit() {
if (this.formControl.value === undefined) { if (this.formControl.value === undefined) {
this.changeDatetime({}); this.changeDatetime({});
} else {
this.datetime = new Date(this.formControl.value);
this.date = this.getDateStruct(this.datetime);
} }
this.formControl.valueChanges.subscribe(value => { this.formControl.valueChanges.subscribe(value => {
this.datetime = new Date(value) if (value) {
this.date = this.getDateStruct(this.datetime); this.datetime = new Date(value);
this.time = this.getTimeStruct(this.datetime); this.date = this.getDateStruct(this.datetime);
this.time = this.getTimeStruct(this.datetime);
} else {
this.datetime = null;
this.date = null;
this.time = null;
}
}) })
} }
@@ -81,15 +89,25 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
} }
changeDatetime(event: any) { changeDatetime(event: any) {
this.datetime.setFullYear(this.date.year) if (this.date && this.time) {
this.datetime.setMonth(this.date.month - 1) if (!this.datetime) {
this.datetime.setDate(this.date.day) this.datetime = new Date();
this.datetime.setHours(this.time.hour) }
this.datetime.setMinutes(this.time.minute) this.datetime.setFullYear(this.date.year)
this.datetime.setSeconds(this.time.second) this.datetime.setMonth(this.date.month - 1)
this.datetime.setDate(this.date.day)
this.datetime.setHours(this.time.hour)
this.datetime.setMinutes(this.time.minute)
this.datetime.setSeconds(this.time.second)
this.formControl.setValue( this.formControl.setValue(
formatDate(this.datetime, 'YYYY-MM-ddTHH:mm:ss.SSS', 'EN_US', 'CET') formatDate(this.datetime, 'YYYY-MM-ddTHH:mm:ss.SSS', 'EN_US', 'CET')
) )
} else {
this.datetime = null;
this.date = null;
this.time = null;
this.formControl.setValue('')
}
} }
} }

View File

@@ -224,7 +224,9 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
result = result.concat(); result = result.concat();
} else if (typeof(obj[k]) == "object") { } else if (typeof(obj[k]) == "object") {
result = result.concat(this.extractParameters(obj[k])); if (obj[k]) {
result = result.concat(this.extractParameters(obj[k]));
}
} }
} }

View File

@@ -1,6 +1,7 @@
import {Component, OnInit} from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormlyFieldInput } from "@ngx-formly/bootstrap/input"; import { FormlyFieldInput } from "@ngx-formly/bootstrap/input";
@Component({ @Component({
selector: 'formly-richtext-type', selector: 'formly-richtext-type',
template: ` template: `
@@ -42,11 +43,20 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
statusbar: false, statusbar: false,
autoresize_bottom_margin: 0, autoresize_bottom_margin: 0,
body_class: "contract-body", body_class: "contract-body",
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }" content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }",
entity_encoding: 'raw',
paste_preprocess: function (plugin: any, args: any) {
console.log(args.content)
let container = document.createElement('div');
container.innerHTML = args.content.trim();
cleanPastedElement(container)
console.log(container.innerHTML);
args.content = container.innerHTML;
}
} }
init_multiline = { init_multiline = {
plugins: 'lists image imagetools table code searchreplace autoresize', plugins: 'lists image imagetools table code searchreplace paste autoresize',
menubar: 'edit insert format tools table', menubar: 'edit insert format tools table',
menu: { menu: {
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' }, edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
@@ -59,7 +69,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
init_singleline = { init_singleline = {
plugins: 'autoresize', plugins: 'paste autoresize',
menubar: '', menubar: '',
toolbar: 'undo redo | bold italic underline', toolbar: 'undo redo | bold italic underline',
} }
@@ -71,7 +81,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
} }
getInitConfig() { getInitConfig(): any {
return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)}; return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)};
} }
@@ -92,7 +102,49 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
} }
} }
} }
}
function cleanPastedElement(htmlElement: HTMLElement): string {
if (! htmlElement.innerHTML) {
return "";
}
let innerHtml = ""
for(let i = 0; i < htmlElement.childNodes.length; i++){
const childNode = htmlElement.childNodes[i] as HTMLElement
if (childNode.nodeName == "#text") {
innerHtml += childNode.nodeValue;
} else {
innerHtml += cleanPastedElement(childNode);
}
}
htmlElement.innerHTML = innerHtml
} if (htmlElement.tagName == "SPAN") {
let text = htmlElement.innerHTML
const style = htmlElement.style
if (style.fontWeight == "700") {
let strong = document.createElement('b');
strong.innerHTML = text
text = strong.outerHTML;
}
if (style.textDecoration == "underline") {
let underline = document.createElement('u');
underline.innerHTML = text;
text = underline.outerHTML;
}
if (style.fontStyle == "italic") {
let italic = document.createElement('em');
italic.innerHTML = text;
text = italic.outerHTML;
}
return text;
}
htmlElement.style.removeProperty("line-height")
htmlElement.style.removeProperty("margin")
return htmlElement.outerHTML
}

View File

@@ -0,0 +1,24 @@
import {Component, OnInit} from "@angular/core";
import { FieldType, FieldTypeConfig } from "@ngx-formly/core";
@Component({
selector: 'app-form-signature-link-type',
template: `
<label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id"
class="form-label">{{ props.label }}
<span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span>
</label>
<div class="input-group mb-12">
<input class="form-control" type="text" readonly disabled="" value="{{ this.signature_url }}"/>
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.signature_url"><i-bs name="text-paragraph"/></button>
</div>
`,
})
export class SignatureLinkTypeComponent extends FieldType<FieldTypeConfig> implements OnInit{
base_path = "/contracts/signature/"
signature_url = ""
ngOnInit() {
this.signature_url = location.origin + this.base_path + this.formControl.value
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -2,10 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>App</title> <title>Cooper, Hillman & Toshi</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<meta property="og:title" content="Cooper, Hillman & Toshi">
<meta property="og:description" content="Interface d'administration des contrats">
<meta property="og:image" content="/assets/logo.png">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@@ -0,0 +1,422 @@
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-US" trgLang="fr">
<file original="ng.template" id="ngi18n">
<unit id="ngb.alert.close">
<segment state="initial">
<source>Close</source>
<target>Fermer</target>
</segment>
</unit>
<unit id="ngb.timepicker.HH">
<segment state="initial">
<source>HH</source>
<target>HH</target>
</segment>
</unit>
<unit id="ngb.toast.close-aria">
<segment state="initial">
<source>Close</source>
<target>Fermer</target>
</segment>
</unit>
<unit id="ngb.pagination.first">
<segment state="initial">
<source>««</source>
<target>««</target>
</segment>
</unit>
<unit id="ngb.datepicker.select-month">
<segment state="initial">
<source>Select month</source>
<target>Selectionner un mois</target>
</segment>
</unit>
<unit id="ngb.datepicker.previous-month">
<segment state="initial">
<source>Previous month</source>
<target>Mois précédent</target>
</segment>
</unit>
<unit id="ngb.progressbar.value">
<segment state="initial">
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
<target>
<ph id="0" equiv="INTERPOLATION"/>
</target>
</segment>
</unit>
<unit id="ngb.carousel.slide-number">
<segment state="initial">
<source> Slide <ph id="0" equiv="INTERPOLATION"/> of <ph id="1" equiv="INTERPOLATION_1"/> </source>
<target> Slide <ph id="0" equiv="INTERPOLATION"/> of <ph id="1" equiv="INTERPOLATION_1"/> </target>
</segment>
</unit>
<unit id="ngb.timepicker.hours">
<segment state="initial">
<source>Hours</source>
<target>Heures</target>
</segment>
</unit>
<unit id="ngb.pagination.previous">
<segment state="initial">
<source>«</source>
<target>«</target>
</segment>
</unit>
<unit id="ngb.carousel.previous">
<segment state="initial">
<source>Previous</source>
<target>Précédent</target>
</segment>
</unit>
<unit id="ngb.timepicker.MM">
<segment state="initial">
<source>MM</source>
<target>MM</target>
</segment>
</unit>
<unit id="ngb.pagination.next">
<segment state="initial">
<source>»</source>
<target>»</target>
</segment>
</unit>
<unit id="ngb.datepicker.select-year">
<segment state="initial">
<source>Select year</source>
<target>Sélectionner une année</target>
</segment>
</unit>
<unit id="ngb.datepicker.next-month">
<segment state="initial">
<source>Next month</source>
<target>Mois suivant</target>
</segment>
</unit>
<unit id="ngb.carousel.next">
<segment state="initial">
<source>Next</source>
<target>Suivant</target>
</segment>
</unit>
<unit id="ngb.timepicker.minutes">
<segment state="initial">
<source>Minutes</source>
<target>Minutes</target>
</segment>
</unit>
<unit id="ngb.pagination.last">
<segment state="initial">
<source>»»</source>
<target>»»</target>
</segment>
</unit>
<unit id="ngb.timepicker.increment-hours">
<segment state="initial">
<source>Increment hours</source>
<target>Incrémenter les heures</target>
</segment>
</unit>
<unit id="ngb.pagination.first-aria">
<segment state="initial">
<source>First</source>
<target>Premier</target>
</segment>
</unit>
<unit id="ngb.pagination.previous-aria">
<segment state="initial">
<source>Previous</source>
<target>Précédent</target>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-hours">
<segment state="initial">
<source>Decrement hours</source>
<target>Décrémenter les heures</target>
</segment>
</unit>
<unit id="ngb.pagination.next-aria">
<segment state="initial">
<source>Next</source>
<target>Suivant</target>
</segment>
</unit>
<unit id="ngb.timepicker.increment-minutes">
<segment state="initial">
<source>Increment minutes</source>
<target>Incrémenter les minutes</target>
</segment>
</unit>
<unit id="ngb.pagination.last-aria">
<segment state="initial">
<source>Last</source>
<target>Dernier</target>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-minutes">
<segment state="initial">
<source>Decrement minutes</source>
<target>Décrémenter les minutes</target>
</segment>
</unit>
<unit id="ngb.timepicker.SS">
<segment state="initial">
<source>SS</source>
<target>SS</target>
</segment>
</unit>
<unit id="ngb.timepicker.seconds">
<segment state="initial">
<source>Seconds</source>
<target>Secondes</target>
</segment>
</unit>
<unit id="ngb.timepicker.increment-seconds">
<segment state="initial">
<source>Increment seconds</source>
<target>Incrémenter les secondes</target>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-seconds">
<segment state="initial">
<source>Decrement seconds</source>
<target>Décrémenter les secondes</target>
</segment>
</unit>
<unit id="ngb.timepicker.PM">
<segment state="initial">
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
<target>
<ph id="0" equiv="INTERPOLATION"/>
</target>
</segment>
</unit>
<unit id="ngb.timepicker.AM">
<segment state="initial">
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
<target>
<ph id="0" equiv="INTERPOLATION"/>
</target>
</segment>
</unit>
<unit id="6570363013146073520">
<segment state="initial">
<source>Dashboard</source>
<target>Tableau de bord</target>
</segment>
</unit>
<unit id="4800190016750145593">
<segment state="initial">
<source>Entities</source>
<target>Clients</target>
</segment>
</unit>
<unit id="3336127935693687033">
<segment state="initial">
<source>Provision&amp;nbsp;Templates</source>
<target>Templates:&amp;nbsp;Clauses</target>
</segment>
</unit>
<unit id="8996893897509995689">
<segment state="initial">
<source>Contracts&amp;nbsp;Templates</source>
<target>Templates:&amp;nbsp;Contrats</target>
</segment>
</unit>
<unit id="4480519554689195945">
<segment state="initial">
<source>Contracts&amp;nbsp;Drafts</source>
<target>Contrats: Brouillons</target>
</segment>
</unit>
<unit id="6325430461732938793">
<segment state="initial">
<source>Contracts</source>
<target>Contrats</target>
</segment>
</unit>
<unit id="3894950702316166331">
<segment state="initial">
<source>Loading...</source>
<target>Chargement...</target>
</segment>
</unit>
<unit id="6621329748219109148">
<segment state="initial">
<source>Duplicate</source>
<target>Dupliquer</target>
</segment>
</unit>
<unit id="6594772639285650443">
<segment state="initial">
<source>Are you sure you want to delete this <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/>?</source>
<target>Êtes-vous sûr de vouloir supprimer ce<ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/>?</target>
</segment>
</unit>
<unit id="2159130950882492111">
<segment state="initial">
<source>Cancel</source>
<target>Annuler</target>
</segment>
</unit>
<unit id="7022070615528435141">
<segment state="initial">
<source>Delete</source>
<target>Supprimer</target>
</segment>
</unit>
<unit id="2502514662075565663">
<segment state="initial">
<source>Duplicate <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/></source>
<target>Dupliquer <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/></target>
</segment>
</unit>
<unit id="5974043874204012120">
<segment state="initial">
<source> Create <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/> </source>
<target> Créer <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/> </target>
</segment>
</unit>
<unit id="8449969376421674433">
<segment state="initial">
<source>Full text search:</source>
<target>Rechercher:</target>
</segment>
</unit>
<unit id="7735090777677032335">
<segment state="initial">
<source>10 items per page</source>
<target>10 par page</target>
</segment>
</unit>
<unit id="2940591680142606604">
<segment state="initial">
<source>15 items per page</source>
<target>15 par page</target>
</segment>
</unit>
<unit id="9121589109350446305">
<segment state="initial">
<source>25 items per page</source>
<target>25 items per page</target>
</segment>
</unit>
<unit id="4791714722376172740">
<segment state="initial">
<source>50 items per page</source>
<target>50 par page</target>
</segment>
</unit>
<unit id="6810640416188824611">
<segment state="initial">
<source>100 items per page</source>
<target>100 par page</target>
</segment>
</unit>
<unit id="2454050363478003966">
<segment state="initial">
<source>Login</source>
<target>Connexion</target>
</segment>
</unit>
<unit id="140822705245800362">
<segment state="initial">
<source>Username:</source>
<target>Utilisateur:</target>
</segment>
</unit>
<unit id="6865009229971482891">
<segment state="initial">
<source>Password:</source>
<target>Mot de passe:</target>
</segment>
</unit>
<unit id="3797778920049399855">
<segment state="initial">
<source>Logout</source>
<target>Déconnexion</target>
</segment>
</unit>
<unit id="5674286808255988565">
<segment state="initial">
<source>Create</source>
<target>Créer</target>
</segment>
</unit>
<unit id="4021752662928002901">
<segment state="initial">
<source>Update</source>
<target>Mettre à jour</target>
</segment>
</unit>
<unit id="6061331044524123789">
<segment state="initial">
<source>Authentication required</source>
<target>Authentification nécessaire</target>
</segment>
</unit>
<unit id="3624268617519726175">
<segment state="initial">
<source>Permissions too low</source>
<target>Permissions trop basses</target>
</segment>
</unit>
<unit id="6833883791906187270">
<segment state="initial">
<source>Download Link:</source>
<target>Lien de téléchargement:</target>
</segment>
</unit>
<unit id="8168599357858858911">
<segment state="initial">
<source>Preview Link:</source>
<target>Lien de prévisualisation:</target>
</segment>
</unit>
<unit id="1295614462098694869">
<segment state="initial">
<source>Preview</source>
<target>Prévisualisation</target>
</segment>
</unit>
<unit id="2445188258613609179">
<segment state="initial">
<source>Signature</source>
<target>Signature</target>
</segment>
</unit>
<unit id="8474971383445371291">
<segment state="initial">
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
<target>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</target>
</segment>
</unit>
<unit id="7430416142942514215">
<segment state="initial">
<source>Publish</source>
<target>Publier</target>
</segment>
</unit>
<unit id="2990108023996257960">
<segment state="initial">
<source>This Contract has already been signed by</source>
<target>Ce contrat a déjà été signé par</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-US">
<file id="ngi18n" original="ng.template">
<unit id="ngb.alert.close">
<segment>
<source>Close</source>
</segment>
</unit>
<unit id="ngb.timepicker.HH">
<segment>
<source>HH</source>
</segment>
</unit>
<unit id="ngb.toast.close-aria">
<segment>
<source>Close</source>
</segment>
</unit>
<unit id="ngb.pagination.first">
<segment>
<source>««</source>
</segment>
</unit>
<unit id="ngb.datepicker.select-month">
<segment>
<source>Select month</source>
</segment>
</unit>
<unit id="ngb.datepicker.previous-month">
<segment>
<source>Previous month</source>
</segment>
</unit>
<unit id="ngb.progressbar.value">
<segment>
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
</segment>
</unit>
<unit id="ngb.carousel.slide-number">
<segment>
<source> Slide <ph id="0" equiv="INTERPOLATION"/> of <ph id="1" equiv="INTERPOLATION_1"/> </source>
</segment>
</unit>
<unit id="ngb.timepicker.hours">
<segment>
<source>Hours</source>
</segment>
</unit>
<unit id="ngb.pagination.previous">
<segment>
<source>«</source>
</segment>
</unit>
<unit id="ngb.carousel.previous">
<segment>
<source>Previous</source>
</segment>
</unit>
<unit id="ngb.timepicker.MM">
<segment>
<source>MM</source>
</segment>
</unit>
<unit id="ngb.pagination.next">
<segment>
<source>»</source>
</segment>
</unit>
<unit id="ngb.datepicker.select-year">
<segment>
<source>Select year</source>
</segment>
</unit>
<unit id="ngb.datepicker.next-month">
<segment>
<source>Next month</source>
</segment>
</unit>
<unit id="ngb.carousel.next">
<segment>
<source>Next</source>
</segment>
</unit>
<unit id="ngb.timepicker.minutes">
<segment>
<source>Minutes</source>
</segment>
</unit>
<unit id="ngb.pagination.last">
<segment>
<source>»»</source>
</segment>
</unit>
<unit id="ngb.timepicker.increment-hours">
<segment>
<source>Increment hours</source>
</segment>
</unit>
<unit id="ngb.pagination.first-aria">
<segment>
<source>First</source>
</segment>
</unit>
<unit id="ngb.pagination.previous-aria">
<segment>
<source>Previous</source>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-hours">
<segment>
<source>Decrement hours</source>
</segment>
</unit>
<unit id="ngb.pagination.next-aria">
<segment>
<source>Next</source>
</segment>
</unit>
<unit id="ngb.timepicker.increment-minutes">
<segment>
<source>Increment minutes</source>
</segment>
</unit>
<unit id="ngb.pagination.last-aria">
<segment>
<source>Last</source>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-minutes">
<segment>
<source>Decrement minutes</source>
</segment>
</unit>
<unit id="ngb.timepicker.SS">
<segment>
<source>SS</source>
</segment>
</unit>
<unit id="ngb.timepicker.seconds">
<segment>
<source>Seconds</source>
</segment>
</unit>
<unit id="ngb.timepicker.increment-seconds">
<segment>
<source>Increment seconds</source>
</segment>
</unit>
<unit id="ngb.timepicker.decrement-seconds">
<segment>
<source>Decrement seconds</source>
</segment>
</unit>
<unit id="ngb.timepicker.PM">
<segment>
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
</segment>
</unit>
<unit id="ngb.timepicker.AM">
<segment>
<source>
<ph id="0" equiv="INTERPOLATION"/>
</source>
</segment>
</unit>
<unit id="2454050363478003966">
<segment>
<source>Login</source>
</segment>
</unit>
<unit id="140822705245800362">
<segment>
<source>Username:</source>
</segment>
</unit>
<unit id="6865009229971482891">
<segment>
<source>Password:</source>
</segment>
</unit>
<unit id="2159130950882492111">
<segment>
<source>Cancel</source>
</segment>
</unit>
<unit id="3797778920049399855">
<segment>
<source>Logout</source>
</segment>
</unit>
<unit id="6570363013146073520">
<segment>
<source>Dashboard</source>
</segment>
</unit>
<unit id="4800190016750145593">
<segment>
<source>Entities</source>
</segment>
</unit>
<unit id="3336127935693687033">
<segment>
<source>Provision&amp;nbsp;Templates</source>
</segment>
</unit>
<unit id="8996893897509995689">
<segment>
<source>Contracts&amp;nbsp;Templates</source>
</segment>
</unit>
<unit id="4480519554689195945">
<segment>
<source>Contracts&amp;nbsp;Drafts</source>
</segment>
</unit>
<unit id="6325430461732938793">
<segment>
<source>Contracts</source>
</segment>
</unit>
<unit id="3894950702316166331">
<segment>
<source>Loading...</source>
</segment>
</unit>
<unit id="6621329748219109148">
<segment>
<source>Duplicate</source>
</segment>
</unit>
<unit id="7022070615528435141">
<segment>
<source>Delete</source>
</segment>
</unit>
<unit id="6594772639285650443">
<segment>
<source>Are you sure you want to delete this <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/>?</source>
</segment>
</unit>
<unit id="2502514662075565663">
<segment>
<source>Duplicate <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/></source>
</segment>
</unit>
<unit id="5674286808255988565">
<segment>
<source>Create</source>
</segment>
</unit>
<unit id="4021752662928002901">
<segment>
<source>Update</source>
</segment>
</unit>
<unit id="5974043874204012120">
<segment>
<source> Create <ph id="0" equiv="INTERPOLATION" disp="{{ this.schema }}"/> </source>
</segment>
</unit>
<unit id="8449969376421674433">
<segment>
<source>Full text search:</source>
</segment>
</unit>
<unit id="7735090777677032335">
<segment>
<source>10 items per page</source>
</segment>
</unit>
<unit id="2940591680142606604">
<segment>
<source>15 items per page</source>
</segment>
</unit>
<unit id="9121589109350446305">
<segment>
<source>25 items per page</source>
</segment>
</unit>
<unit id="4791714722376172740">
<segment>
<source>50 items per page</source>
</segment>
</unit>
<unit id="6810640416188824611">
<segment>
<source>100 items per page</source>
</segment>
</unit>
<unit id="3624268617519726175">
<segment>
<source>Permissions too low</source>
</segment>
</unit>
<unit id="6061331044524123789">
<segment>
<source>Authentication required</source>
</segment>
</unit>
<unit id="6833883791906187270">
<segment>
<source>Download Link:</source>
</segment>
</unit>
<unit id="8168599357858858911">
<segment>
<source>Preview Link:</source>
</segment>
</unit>
<unit id="1295614462098694869">
<segment>
<source>Preview</source>
</segment>
</unit>
<unit id="2445188258613609179">
<segment>
<source>Signature</source>
</segment>
</unit>
<unit id="7430416142942514215">
<segment>
<source>Publish</source>
</segment>
</unit>
<unit id="8474971383445371291">
<segment>
<source>
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Si vous n&apos;êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">pas</pc> <ph id="5" equiv="INTERPOLATION" disp="{{ this.signatory }}"/>, veuillez <pc id="6" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">fermer cette page immédiatement</pc> et surpprimer tous les liens en votre possession menant vers celle-ci.</pc>
<pc id="7" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l&apos;article L.229 du code pénal de l&apos;Etat de San Andreas pour <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="&lt;strong&gt;" dispEnd="&lt;/strong&gt;">usurpation d&apos;identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu&apos;à des poursuites civiles.</pc>
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="&lt;p&gt;" dispEnd="&lt;/p&gt;">Le cabinet Cooper, Hillman &amp; Toshi LLC</pc>
</source>
</segment>
</unit>
<unit id="2990108023996257960">
<segment>
<source>This Contract has already been signed by</source>
</segment>
</unit>
</file>
</xliff>

View File

@@ -26,3 +26,15 @@
.contract-body { .contract-body {
font-family: 'Century Schoolbook'; font-family: 'Century Schoolbook';
} }
.nav-link {
color: #D2BA6F;
--bs-nav-link-hover-color: #9c8a51;
--bs-nav-pills-link-active-color: #D2BA6F;
--bs-nav-pills-link-active-bg: #114856;
}
.nav-link.active {
border: #D2BA6F solid 2px;
border-radius: 0;
}

72
front/nginx.prod.conf Normal file
View File

@@ -0,0 +1,72 @@
worker_processes 1;
events { worker_connections 1024; }
http {
sendfile on;
upstream docker-back {
server back:8000;
}
types {
module js;
}
include /etc/nginx/mime.types;
server {
listen 80;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 256;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html?$args;
}
location ~* ^.+\.css$ {
default_type text/css;
}
location ~* ^.+\.js$ {
default_type text/javascript;
}
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
set $is_robot 1;
}
if ($is_robot = 0) {
rewrite ^ /index.html?$args last;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ {
proxy_pass http://docker-back/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}

View File

@@ -26,6 +26,26 @@ http {
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
} }
location /contracts/signature/ {
set $is_robot 0;
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
proxy_pass http://docker-back;
break;
set $is_robot 1;
}
if ($is_robot = 0) {
proxy_pass http://docker-front;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/v1/ { location /api/v1/ {
proxy_pass http://docker-back/; proxy_pass http://docker-back/;
proxy_redirect off; proxy_redirect off;