106 Commits

Author SHA1 Message Date
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 4237 additions and 821 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
__pycache__/
back/app/fixtures/
back/media/
front/app-back/
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
# copy both 'package.json' and 'package-lock.json' (if available)
COPY ./requirements.txt /code/requirements.txt
# install project dependencies
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
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 .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"], )
contract_router.include_router(print_router, prefix="/print", tags=["print"], )
from ..entity.models import Entity
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 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 ..entity.models import Entity
class ContractStatus(str, Enum):
new = 'new'
published = 'published'
signed = 'signed'
in_effect = 'in_effect'
printed = 'printed'
executed = 'executed'
class ContractDraftStatus(str, Enum):
draft = 'draft'
created = 'created'
in_progress = 'in_progress'
ready = 'ready'
published = 'published'
class Party(BaseModel):
class DraftParty(BaseModel):
entity_id: str = Field(
foreignKey={
"reference": {
"resource": "entity",
"schema": "Entity",
}
}
},
default="",
title="Partie"
)
part: str
part: str = Field(title="Rôle")
representative_id: str = Field(
foreignKey={
"reference": {
@@ -36,14 +41,30 @@ class Party(BaseModel):
"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):
type: Literal['genuine'] = 'genuine'
title: str = RichtextSingleline(props={"parametrized": True})
body: str = RichtextMultiline(props={"parametrized": True})
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
class Config:
title = 'Clause personalisée'
class ContractProvisionTemplateReference(BaseModel):
@@ -56,23 +77,157 @@ class ContractProvisionTemplateReference(BaseModel):
"displayedFields": ['title', 'body']
},
},
props={"parametrized": True}
props={"parametrized": True},
default="",
title="Template de clause"
)
class Config:
title = 'Template de clause'
class DraftProvision(BaseModel):
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):
name: str
title: str
parties: List[Party]
provisions: List[DraftProvision]
"""
Brouillon de contrat à remplir
"""
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[DraftParty] = Field(title="Parties")
provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
title='Variables'
)
status: ContractDraftStatus = Field(default=ContractDraftStatus.draft)
location: str = ""
date: datetime.date = datetime.date(1970, 1, 1)
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
todo: List[str] = Field(default=[], title="Reste à faire")
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class Config:
title = 'Brouillon de contrat'
async def check_is_ready(self):
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.templating import Jinja2Templates
@@ -9,7 +13,7 @@ from pathlib import Path
from app.entity.models import Entity
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):
@@ -24,19 +28,28 @@ async def build_model(model):
parties.append(party)
model.parties = parties
provisions = []
for p in model.provisions:
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:
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.location = "Toulouse"
model.date = "01/01/1970"
model = model.dict()
model['location'] = "Los Santos, SA"
model['date'] = datetime.date(1970, 1, 1)
model['lawyer'] = {'entity_data': {
"firstname": "prénom avocat",
"lastname": "nom avocat",
}}
return model
@@ -48,49 +61,96 @@ print_router = APIRouter()
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")
return template.render({
"draft": draft,
"lawyer": lawyer,
"static_host": host
"contract": contract,
"root_url": root_url
})
async def render_css(host, draft):
async def render_css(root_url, contract):
template = templates.get_template("styles.css")
return template.render({
"draft": draft,
"static_host": host
"contract": contract,
"root_url": root_url
})
@print_router.get("/", response_class=HTMLResponse)
async def create() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
@print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse)
async def preview_draft(draft_id: str, request: Request) -> str:
draft = await build_model(await ContractDraft.get(draft_id))
return await render_print('localhost', draft, lawyer)
return await render_print('', draft)
@print_router.get("/pdf", response_class=FileResponse)
async def create_pdf() -> str:
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
lawyer = {
"firstname": "Nathaniel",
"lastname": "Toshi",
}
@print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
async def preview_contract_by_signature(signature_id: str, request: Request) -> str:
contract = await Contract.find_by_signature_id(signature_id)
for p in contract.parties:
if p.signature_affixed:
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
font_config = FontConfiguration()
html = HTML(string=await render_print('nginx', draft, lawyer))
css = CSS(string=await render_css('nginx', draft), font_config=font_config)
return await render_print('', contract)
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(
"out.pdf",
contract_path,
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 id="front-page-header">
<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>
</tr></table>
<h1>{{ draft.title|upper }}</h1>
<h1>{{ contract.title|upper }}</h1>
</div>
<div class="intro">
<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>
{% for party in draft.parties %}
{% for party in contract.parties %}
<div class="party">
{% if not loop.first %}
<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 %}
{% 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.entity_data.day_of_birth %} n&eacute; le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if party.entity.entity_data.place_of_birth %} &agrave; {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
{% if party.entity.address %} r&eacute;sidant &agrave; {{ party.entity.address }}, {% endif %}
{% elif party.entity.entity_data.type == "institution" %}
@@ -40,14 +40,14 @@
</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>Sous la supervision l&eacute;gale de Ma&icirc;tre <strong>{{ contract.lawyer.entity_data.firstname }} {{ contract.lawyer.entity_data.lastname }}</strong></p>
<p>Il a &eacute;t&eacute; convenu l&apos;ex&eacute;cution des prestations ci-dessous, conform&eacute;ment aux conditions g&eacute;n&eacute;rales et particuli&egrave;res ci-apr&egrave;s:</p>
</div>
</div>
<div class="content">
<h2>Conditions g&eacute;n&eacute;rales & particuli&egrave;res</h2>
{% for provision in draft.provisions %}
{% for provision in contract.provisions %}
<div class="provision">
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
<p>{{ provision.body|safe }}</p>
@@ -56,11 +56,18 @@
<div class="footer">
<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>
<table class="signatures">
<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>
</table>
</div>

View File

@@ -1,24 +1,24 @@
@font-face {
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-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-face {
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-face {
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-style: italic;
}
@@ -28,10 +28,10 @@
margin: 2cm 2cm 2cm 2cm;
counter-increment: page;
@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;
}
background: url('http://{{ static_host }}/assets/watermark.png') no-repeat;
background: url('{{ root_url }}/assets/watermark.png') no-repeat;
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 .models import ContractDraft
from ..user.manager import get_current_user
from .models import ContractDraft, ContractDraftStatus
from .schemas import 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 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 ..core.schemas import Writer
@@ -15,13 +15,17 @@ class ContractDraftRead(ContractDraft):
class ContractDraftCreate(Writer):
name: str
title: str
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(
default=[],
format="dictionary",
title='Variables'
)
async def validate_foreign_key(self):
@@ -34,3 +38,38 @@ class ContractDraftCreate(Writer):
class ContractDraftUpdate(ContractDraftCreate):
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):
_id: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
updated_at: datetime = Field(default_factory=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, title="Modifié le")
@validator("label", always=True, check_fields=False)
def generate_label(cls, v, values, **kwargs):

View File

@@ -1,10 +1,13 @@
from beanie import PydanticObjectId
from beanie.odm.operators.find.comparison import In
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.ext.motor import paginate
from ..user.manager import get_current_user, get_current_superuser
def parse_sort(sort_by):
if not sort_by:
@@ -36,16 +39,21 @@ def parse_query(query: str, model):
or_array = []
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]
elif operator == 'eq':
operand = Eq(column, value)
elif operator == 'in':
operand = In(column, value.split(','))
and_array.append(operand)
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:
return {}
@@ -55,18 +63,19 @@ def get_crud_router(model, model_create, model_read, model_update):
router = APIRouter()
@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()
o = await model(**item.dict()).create()
return {"message": "{} added successfully".format(model.__name__), "id": o.id}
@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)
return model_read(**item.dict())
@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)
query = parse_query(query, model_read)
@@ -75,7 +84,7 @@ def get_crud_router(model, model_create, model_read, model_update):
return await items
@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}
update_query = {"$set": {
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())
@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)
if not item:

View File

@@ -5,10 +5,11 @@ from beanie import init_beanie
from .user import User, AccessToken
from .entity.models import Entity
from .template.models import ContractTemplate, ProvisionTemplate
from .order.models import Order
from .contract.models import ContractDraft
from .contract.models import ContractDraft, Contract
# 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():
@@ -17,5 +18,6 @@ async def init_db():
)
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)

View File

@@ -15,51 +15,68 @@ class EntityType(BaseModel):
class Individual(EntityType):
type: Literal['individual'] = 'individual'
firstname: Indexed(str)
middlename: Indexed(str) = ""
lastname: Indexed(str)
surnames: List[Indexed(str)] = []
day_of_birth: date
place_of_birth: str = ""
firstname: Indexed(str) = Field(title='Prénom')
middlename: Indexed(str) = Field(default="", title='Autres prénoms')
lastname: Indexed(str) = Field(title='Nom de famille')
surnames: List[Indexed(str)] = Field(
default=[],
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
def label(self) -> str:
if len(self.surnames) > 0:
return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
# if len(self.surnames) > 0:
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
return '{} {}'.format(self.firstname, self.lastname)
class Config:
title = 'Particulier'
class Employee(BaseModel):
role: Indexed(str)
role: Indexed(str) = Field(title='Poste')
entity_id: str = Field(foreignKey={
"reference": {
"resource": "entity",
"schema": "Entity",
"condition": "entity_data.type=individual"
}
})
},
title='Employé'
)
class Config:
title = 'Fiche Employé'
class Corporation(EntityType):
type: Literal['corporation'] = 'corporation'
title: Indexed(str)
activity: Indexed(str)
employees: List[Employee] = Field(default=[])
title: Indexed(str) = Field(title='Dénomination sociale')
activity: Indexed(str) = Field(title='Activité')
employees: List[Employee] = Field(default=[], title='Employés')
class Config:
title = 'Entreprise'
class Institution(EntityType):
class Institution(Corporation):
type: Literal['institution'] = 'institution'
title: Indexed(str)
activity: Indexed(str)
employees: List[Employee] = Field(default=[])
class Config:
title = 'Institution'
class Entity(CrudDocument):
"""
Fiche d'un client
"""
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
label: str = None
address: Optional[str] = ""
address: str = Field(default="", title='Adresse')
@validator("label", always=True)
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,
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):
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):
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: Optional[str] = ""
class EntityUpdate(EntityCreate):
pass

View File

@@ -4,8 +4,8 @@ from .contract import contract_router
from .db import init_db
from .user import user_router, get_auth_router
from .entity import entity_router
from .order import order_router
from .template import template_router
# from .order import order_router
app = FastAPI(root_path="/api/v1")
@@ -15,17 +15,12 @@ async def on_startup():
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(user_router, prefix="/users", tags=["users"], )
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(contract_router, prefix="/contract", tags=["contract"], )
# app.include_router(order_router, prefix="/order", tags=["order"], )
if __name__ == '__main__':
import uvicorn

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import uuid
from typing import Any, Dict, Generic, Optional
from typing import Any
from bson import ObjectId
from fastapi import Depends
@@ -88,10 +88,10 @@ async def get_user_manager(user_db=Depends(get_user_db)):
def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> 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(
@@ -107,6 +107,7 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
)
get_current_user = fastapi_users.current_user(active=True)
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
def get_auth_router():

View File

@@ -1,6 +1,6 @@
from typing import Optional, TypeVar
from datetime import datetime
from pydantic import BaseModel, Field
from pydantic import Field
from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
@@ -15,6 +15,7 @@ class AccessToken(BeanieBaseAccessToken[PydanticObjectId]):
class User(BeanieBaseUser[PydanticObjectId]):
login: str
entity_id: str
created_at: datetime = Field(default=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 .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.post("/", response_description="User added to the database")
async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> dict:
await user_manager.create(user, safe=True)
async def create(user_form: UserCreate, user_manager=Depends(get_user_manager), user=Depends(get_current_superuser)) -> dict:
await user_manager.create(user_form, safe=True)
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")
async def read_id(id: PydanticObjectId) -> UserRead:
async def read_id(id: PydanticObjectId, user=Depends(get_current_superuser)) -> UserRead:
user = await User.get(id)
return UserRead(**user.dict())
@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()
return users
@router.put("/{id}", response_description="User record updated")
async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead:
req = {k: v for k, v in req.dict().items() if v is not None}
async def update(id: PydanticObjectId, user_form: UserUpdate, user=Depends(get_current_superuser)) -> UserRead:
user_form = {k: v for k, v in user_form.dict().items() if v is not None}
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)
@@ -56,7 +56,7 @@ async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead:
@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)
if not record:

View File

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

View File

View File

View File

@@ -3,10 +3,10 @@ import asyncio
import json
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):

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

View File

@@ -1,22 +1,11 @@
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
# copy both 'package.json' and 'package-lock.json' (if available)
RUN npm install -g @angular/cli http-server
COPY app/package*.json ./
# install project dependencies
RUN npm install -g @angular/cli
RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY app/ .
# build app for production with minification
RUN npm run build
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": "",
"sourceRoot": "src",
"prefix": "app",
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": ""
}
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"localize": ["fr"],
"i18nMissingTranslation": "warning",
"outputPath": "dist/app",
"index": "src/index.html",
"main": "src/main.ts",
@@ -65,14 +76,22 @@
},
"development": {
"browserTarget": "app:build:development"
},
"fr": {
"browserTarget": "app:build:fr"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
"options": {
"browserTarget": "app:build"
"browserTarget": "app:build",
"format": "xlf2",
"outputPath": "src/locale",
"targetFiles": [
"messages.fr.xlf"
]
}
},
"test": {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {ContractsModule} from "./views/contracts/contracts.module";
const routes: Routes = [
{
@@ -32,6 +31,11 @@ const routes: Routes = [
},
{
path: 'contract-drafts',
loadChildren: () =>
import('./views/contract-drafts/contract-drafts.module').then((m) => m.ContractDraftsModule)
},
{
path: 'contracts',
loadChildren: () =>
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;">
<div class="container-fluid">
<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>
</div>
<div class="col py-3">
@@ -13,3 +13,4 @@
</div>
</div>
</main>

View File

@@ -1,10 +1,15 @@
import { Component } from '@angular/core';
import {Title} from "@angular/platform-browser";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
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 { NgbModule } from '@ng-bootstrap/ng-bootstrap';
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 { AppComponent } from './app.component';
@@ -9,20 +11,31 @@ import { AppComponent } from './app.component';
import { SidenavComponent } from "./layout/sidenav/sidenav.component";
import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component";
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({
declarations: [
AppComponent,
SidenavComponent,
FlashmessagesComponent
FlashmessagesComponent,
LoginComponent,
LogoutComponent
],
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
HttpClientModule,
],
providers: [
FlashmessagesService,
AuthService,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
providers: [FlashmessagesService],
bootstrap: [AppComponent]
})
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">
<ngb-toast
*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)"
>
<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">
<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>
<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">
<a class="nav-link px-3 w-100" routerLink="{{item.link}}" [class.active]="is_current_page(item)">
<i-bs [name]="item.icon"></i-bs><span class="ms-1 d-none d-sm-inline" [innerHTML]="item.title"></span>
</a>
</li>
<li><login></login></li>
<ng-container *ngIf="isAuthenticated">
<li *ngFor="let item of Menu" class="nav-item w-100">
<a class="nav-link px-3 w-100" routerLink="{{item.link}}" [class.active]="is_current_page(item)">
<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>
</div>

View File

@@ -1,6 +1,15 @@
import { Component } from "@angular/core";
import { Router } from '@angular/router';
import { IconNamesEnum } from "ngx-bootstrap-icons";
import { AuthService } from "../auth/auth.service";
interface MenuItem {
title: string,
link: string,
icon: IconNamesEnum
}
@Component({
selector: "sidenav",
@@ -8,37 +17,52 @@ import { IconNamesEnum } from "ngx-bootstrap-icons";
styleUrls: ["./sidenav.component.css"]
})
export class SidenavComponent {
Menu = [
Menu: MenuItem[] = [
{
title: "Dashboard",
title: $localize`Dashboard`,
link: "/dashboard",
icon: IconNamesEnum.HouseFill
},
{
title: "Entities",
title: $localize`Entities`,
link: "/entities",
icon: IconNamesEnum.PeopleFill
},
{
title: "Provision&nbsp;Templates",
title: $localize`Provision&nbsp;Templates`,
link: "/templates/provisions",
icon: IconNamesEnum.BlockquoteLeft
},
{
title: "Contracts&nbsp;Templates",
title: $localize`Contracts&nbsp;Templates`,
link: "/templates/contracts",
icon: IconNamesEnum.FileCodeFill
},
{
title: "Contracts&nbsp;Drafts",
title: $localize`Contracts&nbsp;Drafts`,
link: "/contract-drafts",
icon: IconNamesEnum.PencilSquare
},
{
title: $localize`Contracts`,
link: "/contracts",
icon: IconNamesEnum.FileEarmarkTextFill
},
]
constructor(private router: Router) {}
isAuthenticated: boolean = false
is_current_page(menu_item: any) {
return this.router.url.indexOf(menu_item.link) > -1;
constructor(
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"
(resourceUpdated)="this.flashService.success('Entity updated')"
(resourceDeleted)="this.flashService.success('Entity deleted')"
(resourceReceived)="this.onResourceReceived($event)"
(error)="this.flashService.error($event)">
</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 { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service";
@Component({
templateUrl: 'card.component.html',
selector: 'base-card',
templateUrl: 'card.component.html',
selector: 'base-card',
})
export class BaseCrudCardComponent {
@Input() resource: string | undefined;
@Input() resource_id: string | null = null;
@Input() schema: string | undefined;
@Input() resource: string | undefined;
@Input() resource_id: string | null = null;
@Input() schema: string | undefined;
constructor(
private route: ActivatedRoute,
public flashService: FlashmessagesService
) {}
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
ngOnInit(): void {
if (this.resource_id === null) {
this.route.paramMap.subscribe((params: ParamMap) => {
this.resource_id = params.get('id')
})
constructor(
private route: ActivatedRoute,
public flashService: FlashmessagesService
) {}
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"
[schema]="this.schema"
[columns]="this.columns"
[filters]="this.filters"
(result)="this.flashService.success($event)"
(error)="this.flashService.error($event)">
</crud-list>

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<base-list
[resource]="this.resource"
[schema]="this.schema"
[columns]="this.columns">
[columns]="this.columns"
[filters]="this.filters">
</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 { Routes, RouterModule } from '@angular/router';
import { DraftCardComponent, DraftListComponent, DraftNewComponent } from "./drafts.component";
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component";
const routes: Routes = [
{
path: '',
data: {
title: 'Entities',
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: DraftListComponent,
{
path: '',
data: {
title: 'List',
title: 'Contracts',
},
},
{
path: 'new',
component: DraftNewComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: DraftCardComponent,
data: {
title: 'Card',
},
},
],
},
children: [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{ path: 'drafts', redirectTo: '/contract-drafts/list' },
{
path: 'list',
component: ContractsListComponent,
data: {
title: 'List',
},
},
{
path: 'new',
component: ContractsNewComponent,
data: {
title: 'New',
},
},
{
path: 'signature/:id',
component: ContractsSignatureComponent,
data: {
title: 'New',
},
},
{
path: ':id',
component: ContractsCardComponent,
data: {
title: 'Card',
},
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
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 { ContractsRoutingModule } from './contracts-routing.module';
import { DraftCardComponent, DraftListComponent, DraftNewComponent, DraftNewFormComponent } 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 } 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({
@@ -15,20 +20,27 @@ import { CrudService } from "@common/crud/crud.service";
CommonModule,
BaseViewModule,
ContractsRoutingModule,
NgbAccordionModule,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
FormlyModule.forRoot({
types: [
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
]
}),
FormlyBootstrapModule,
ClipboardModule,
],
declarations: [
DraftListComponent,
DraftNewComponent,
DraftCardComponent,
DraftNewFormComponent
ContractsListComponent,
ContractsNewComponent,
ContractsCardComponent,
ContractsSignatureComponent,
SignatureDrawerComponent,
BlackBlueRangeComponent,
AlphaRangeComponent
],
providers: [CrudService]
providers: [CrudService, ImageUploaderCrudService]
})
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 {
columns = ['label', 'address', 'entity_data.type']
filters = [];
}
@Component({

View File

@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EntitiesRoutingModule } from './entities-routing.module';
import { EntityCardComponent, EntityListComponent, EntityNewComponent} from "./entities.component";
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({
templateUrl: '../base-view/templates/list.template.html'
templateUrl: '../base-view/templates/list.template.html'
})
export class ContractTemplateListComponent extends BaseContractTemplateComponent {
columns = [];
columns = ['name', 'title', 'parties.items.part'];
filters = [];
}
@Component({
templateUrl: '../base-view/templates/new.template.html'
templateUrl: '../base-view/templates/new.template.html'
})
export class ContractTemplateNewComponent extends BaseContractTemplateComponent {
}
@Component({
templateUrl: '../base-view/templates/card.template.html'
templateUrl: '../base-view/templates/card.template.html'
})
export class ContractTemplateCardComponent extends BaseContractTemplateComponent {
}

View File

@@ -7,20 +7,21 @@ export class BaseProvisionTemplateComponent {
}
@Component({
templateUrl: '../base-view/templates/list.template.html'
templateUrl: '../base-view/templates/list.template.html'
})
export class ProvisionTemplateListComponent extends BaseProvisionTemplateComponent{
columns = [];
columns = ['name', 'title', 'body'];
filters = [];
}
@Component({
templateUrl: '../base-view/templates/new.template.html'
templateUrl: '../base-view/templates/new.template.html'
})
export class ProvisionTemplateNewComponent extends BaseProvisionTemplateComponent {
}
@Component({
templateUrl: '../base-view/templates/card.template.html'
templateUrl: '../base-view/templates/card.template.html'
})
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>
<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>
<div class="d-grid gap-2 d-md-flex">
<button class="btn btn-success btn-lg" type="submit"
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)">
{{ this.isCreateForm() ? "Create" : "Update" }}
{{ submitText }}
</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)"
(click)="open(duplicationModal)">
Duplicate
</button>
<button class="btn btn-danger btn-lg" type="button" *ngIf="!this.isCreateForm()"
(click)="open(duplicationModal)">Duplicate</button>
<button class="btn btn-danger btn-lg" i18n type="button" *ngIf="!this.isCreateForm()"
[disabled]="formLoading$ || modelLoading$ | async"
(click)="open(confirmDeleteModal)">
Delete
</button>
(click)="open(confirmDeleteModal)">Delete</button>
<ng-template #confirmDeleteModal let-modal>
<div class="modal-header">
<h4 class="modal-title">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>
<h4 class="modal-title" i18n>Are you sure you want to delete this {{ this.schema }}?</h4>
<button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
</div>
<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-danger" (click)="modal.close('Save click')">Delete</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" i18n (click)="modal.close('Save click')">Delete</button>
</div>
</ng-template>
<ng-template #duplicationModal let-modal>
<div class="modal-header">
<h4 class="modal-title">Duplicate {{ this.schema }}</h4>
<button type="button" class="btn-close" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
<h4 class="modal-title" i18n>Duplicate {{ this.schema }}</h4>
<button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
</div>
<div class="modal-body">
<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";
@Component({
selector: 'crud-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
selector: 'crud-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {
@Input() resource: string | undefined;
@Input() resource_id: string | null = null;
@Input() schema: string | undefined;
@Input() is_modal: Boolean = false;
@Input() resource: string | undefined;
@Input() resource_id: string | null = null;
@Input() schema: string | undefined;
@Input() is_modal: Boolean = false;
private _model: {} = {};
@@ -37,12 +37,12 @@ export class CardComponent implements OnInit {
@Output() resourceUpdated: EventEmitter<string> = new EventEmitter();
@Output() resourceDeleted: EventEmitter<string> = new EventEmitter();
@Output() error: EventEmitter<string> = new EventEmitter();
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
form = new FormGroup({});
fields: FormlyFieldConfig[] = [];
form = new FormGroup({});
fields: FormlyFieldConfig[] = [];
schemas = JSON.parse(`{}`);
schemas = JSON.parse(`{}`);
private _formLoading$ = new BehaviorSubject<boolean>(true);
private _modelLoading$ = new BehaviorSubject<boolean>(true);
@@ -55,12 +55,16 @@ export class CardComponent implements OnInit {
return this._modelLoading$.asObservable();
}
constructor(private crudService: CrudService,
private formlyJsonschema: CrudFormlyJsonschemaService,
private router: Router,
private route: ActivatedRoute,
private modalService: NgbModal,
) { }
get submitText() {
return this.isCreateForm() ? $localize`Create` : $localize`Update`
}
constructor(private crudService: CrudService,
private formlyJsonschema: CrudFormlyJsonschemaService,
private router: Router,
private route: ActivatedRoute,
private modalService: NgbModal,
) { }
ngOnInit(): void {
this._formLoading$.next(true);
@@ -74,6 +78,7 @@ export class CardComponent implements OnInit {
next :(model: any) => {
this.model = model;
this._modelLoading$.next(false);
this.resourceReceived.emit(model);
},
error: (err) => this.error.emit("Error loading the model:" + err)
});
@@ -92,27 +97,28 @@ export class CardComponent implements OnInit {
onSubmit(model: any) {
this._modelLoading$.next(true);
if (this.isCreateForm()) {
this.crudService.create(this.resource!, model).subscribe({
next: (response: any) => {
this._modelLoading$.next(false);
if (! this.is_modal) {
this.router.navigate([`../${response.id}`], {relativeTo: this.route});
} else {
this.resourceCreated.emit(response.id)
}
},
error: (err) => this.error.emit("Error creating the entity:" + err)
});
this.crudService.create(this.resource!, model).subscribe({
next: (response: any) => {
this._modelLoading$.next(false);
if (! this.is_modal) {
this.router.navigate([`../${response.id}`], {relativeTo: this.route});
} else {
this.resourceCreated.emit(response.id)
}
},
error: (err) => this.error.emit("Error creating the entity:" + err)
});
} else {
model._id = this.resource_id;
this.crudService.update(this.resource!, model).subscribe( {
next: (model: any) => {
this.model = model;
this._modelLoading$.next(false);
this.resourceUpdated.emit(model._id)
},
error: (err) => this.error.emit("Error updating the entity:" + err)
});
this.crudService.update(this.resource!, model).subscribe( {
next: (model: any) => {
this.resourceUpdated.emit(model._id);
this.resourceReceived.emit(model);
this.model = model;
this._modelLoading$.next(false);
},
error: (err) => this.error.emit("Error updating the entity:" + err)
});
}
}
@@ -136,9 +142,9 @@ export class CardComponent implements OnInit {
this.modalService.dismissAll();
}
isCreateForm() {
return this.resource_id === null;
}
isCreateForm() {
return this.resource_id === null;
}
open(content: any) {
this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then(

View File

@@ -43,13 +43,20 @@ export class CrudFormlyJsonschemaOptions implements FormlyJsonschemaOptions {
field.type = "datetime";
} else if (schema.format === '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";
} else if (schema.type == "array" && schema.format == "dictionary") {
field.type = "dictionary";
} else if (schema.type == "string" && schema.hasOwnProperty('props')
&& schema.props.hasOwnProperty("richtext") && schema.props.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')) {

View File

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

View File

@@ -15,7 +15,7 @@ import { ArrayTypeComponent } from "./types/array.type";
import { ObjectTypeComponent } from "./types/object.type";
import { DatetimeTypeComponent } from "./types/datetime.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 { NgbModule} from "@ng-bootstrap/ng-bootstrap";
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
@@ -26,12 +26,16 @@ import { HiddenTypeComponent } from "./types/hidden.type";
import { DictionaryTypeComponent } from "./types/dictionary.type";
import { DictionaryService } from "./types/dictionary.service";
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({
declarations: [
CardComponent,
ListComponent,
FilterListComponent,
ObjectTypeComponent,
DatetimeTypeComponent,
DateTypeComponent,
@@ -40,12 +44,14 @@ import { RichtextTypeComponent } from "./types/richtext.type";
ForeignkeyTypeComponent,
HiddenTypeComponent,
DictionaryTypeComponent,
RichtextTypeComponent
RichtextTypeComponent,
SignatureLinkTypeComponent
],
providers: [
JsonschemasService,
ApiService,
CrudService,
ImageUploaderCrudService,
CrudFormlyJsonschemaService,
DictionaryService
],
@@ -68,10 +74,12 @@ import { RichtextTypeComponent } from "./types/richtext.type";
{ name: 'hidden', component: HiddenTypeComponent },
{ name: 'dictionary', component: DictionaryTypeComponent },
{ name: 'richtext', component: RichtextTypeComponent },
{ name: 'signature-link', component: SignatureLinkTypeComponent },
]
}),
FormlyBootstrapModule,
EditorModule
EditorModule,
ClipboardModule
],
exports: [
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) {
let resource;
resource = { ... this.rawSchemas.components.schemas[resourceName]};
resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
@@ -94,12 +94,34 @@ export class JsonschemasService {
}
}
}
this.changePropertiesOrder(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) {
return prop.hasOwnProperty('properties')
}
@@ -140,6 +162,14 @@ export class JsonschemasService {
}
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");
return false;
@@ -159,7 +189,15 @@ export class JsonschemasService {
} else if (this.is_union(resource)) {
for (const ref of resource.oneOf!) {
// @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
return this.get_descendant(ref, property_name);
}
@@ -184,6 +222,21 @@ export class JsonschemasService {
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 {

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>
<button class="btn btn-success btn-lg float-end" type="button" (click)="onCreate()">
<button class="btn btn-success btn-lg float-end" type="button" i18n (click)="onCreate()">
Create {{ this.schema }}
</button>
<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">
<input
id="table-complete-search"
@@ -14,19 +13,27 @@
[(ngModel)]="searchTerm"
/>
</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>
<span class="col col-form-label" i18n *ngIf="loading$ | async">Loading...</span>
</div>
<div class="table-responsive-md">
<table class="table table-striped">
<table class="table table-striped table-hover">
<thead>
<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>
</thead>
<tbody>
<tr *ngFor="let row of listData$ | async" (click)="onSelect(row._id)">
<td *ngFor="let col of this.displayedColumns">
<ngb-highlight [result]="getColumnValue(row,col)" [term]="searchTerm"></ngb-highlight>
<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>
</tr>
</tbody>
@@ -38,11 +45,11 @@
</ngb-pagination>
<select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="pageSize">
<option [ngValue]="10">10 items per page</option>
<option [ngValue]="15">15 items per page</option>
<option [ngValue]="25">25 items per page</option>
<option [ngValue]="50">50 items per page</option>
<option [ngValue]="100">100 items per page</option>
<option i18n [ngValue]="10">10 items per page</option>
<option i18n [ngValue]="15">15 items per page</option>
<option i18n [ngValue]="25">25 items per page</option>
<option i18n [ngValue]="50">50 items per page</option>
<option i18n [ngValue]="100">100 items per page</option>
</select>
</div>
</form>

View File

@@ -13,6 +13,12 @@ interface State {
searchTerm: string;
sortColumn: SortColumn;
sortDirection: SortDirection;
searchFilters: {[key: string]: any}
}
interface Column {
path: string,
title: string
}
@Component({
@@ -23,6 +29,7 @@ interface State {
export class ListComponent implements OnInit {
@Input() resource: string = "";
@Input() columns: string[] = [];
@Input() filters: string[] = [];
@Input() schema: string | undefined;
@Output() error: EventEmitter<string> = new EventEmitter();
@@ -30,10 +37,9 @@ export class ListComponent implements OnInit {
@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader> = new QueryList<NgbdSortableHeader>();
public displayedColumns: string[] = [];
public displayedColumns: Column[] = [];
private _loading$ = new BehaviorSubject<boolean>(true);
//private _search$ = new Subject<void>();
private _listData$ = new BehaviorSubject<any[]>([]);
private _total$ = new BehaviorSubject<number>(0);
@@ -43,6 +49,7 @@ export class ListComponent implements OnInit {
searchTerm: '',
sortColumn: '_id',
sortDirection: 'asc',
searchFilters: {}
};
constructor(private service: CrudService,
@@ -56,37 +63,63 @@ export class ListComponent implements OnInit {
next: (schema: any) => this.getColumnDefinition(schema),
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._total$.next(this.page * this.pageSize);
this._set(parsedParams)
});
}
getColumnDefinition(schema: JSONSchema7) {
for (let column of this.columns) {
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) {
for (let param_name in schema.properties) {
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;
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() {
this._loading$.next(true);
let sortBy = new SortBy(this.sortColumn, this.sortDirection)
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({
next: (data: any) => {
@@ -101,6 +134,10 @@ export class ListComponent implements OnInit {
});
}
onFilterChange(event: any) {
this.searchFilters = event;
}
onSort({ column, direction }: any) {
// resetting other headers
this.headers.forEach((header) => {
@@ -113,10 +150,15 @@ export class ListComponent implements OnInit {
this.sortDirection = direction;
}
onSelect(id: string) {
onRowClick(id: string) {
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() {
this.router.navigate([`../new`], {relativeTo: this.route});
}
@@ -145,25 +187,44 @@ export class ListComponent implements OnInit {
get searchTerm() {
return this._state.searchTerm;
}
set page(page: number) {
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 });
get searchFilters() {
return this._state.searchFilters;
}
private _set(patch: Partial<State>) {
Object.assign(this._state, patch);
this._search();
}
set page(page: number) {
this.updateState({ page });
}
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">
<strong>{{ i + 1 }}</strong>
</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 == 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>
@@ -29,7 +29,7 @@ import { FieldArrayType } from '@ngx-formly/core';
</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>
`,
})

View File

@@ -1,7 +1,6 @@
import { Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import { Component, OnInit } from '@angular/core';
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';
@@ -15,12 +14,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
<formly-validation-message [field]="field"></formly-validation-message>
</div>
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
/>
<div class="input-group" *ngIf="! this.field.props.readonly">
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
[class.is-invalid]="showError"
/>
<button class="btn btn-outline-secondary" (click)="d.toggle()" type="button"><i-bs name="calendar-date-fill"></i-bs></button>
<input
class="form-control"
placeholder="yyyy-mm-dd"
@@ -29,33 +28,40 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
(ngModelChange)="changeDatetime($event)"
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 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>
`,
})
export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
{
public date : NgbDateStruct;
public datetime : Date = new Date();
public date : NgbDateStruct | null = null;
public datetime : Date | null = null;
constructor() {
super();
this.date = this.getDateStruct(new Date());
}
ngOnInit() {
if (this.formControl.value === undefined) {
this.changeDatetime({});
} else {
this.datetime = new Date(this.formControl.value);
this.date = this.getDateStruct(this.datetime);
}
this.formControl.valueChanges.subscribe(value => {
this.datetime = new Date(value)
this.date = this.getDateStruct(this.datetime);
if (value) {
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) {
this.datetime.setFullYear(this.date.year)
this.datetime.setMonth(this.date.month - 1)
this.datetime.setDate(this.date.day)
if (this.date) {
if (!this.datetime) {
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(
formatDate(this.datetime, 'YYYY-MM-dd', 'EN_US', 'CET')
)
this.formControl.setValue(
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, ElementRef, OnInit, ViewChild} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { formatDate } from "@angular/common";
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
@@ -12,12 +11,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
class="form-label">{{ props.label }}
<span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span>
</label>
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
/>
<div class="input-group" *ngIf="! this.field.props.readonly">
<input type="hidden"
[formControl]="formControl"
[formlyAttributes]="field"
[class.is-invalid]="showError"
/>
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
<input
class="form-control"
placeholder="yyyy-mm-dd"
@@ -26,8 +25,8 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
(ngModelChange)="changeDatetime($event)"
ngbDatepicker
#d="ngbDatepicker"
[class.is-invalid]="showError"
/>
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
<ngb-timepicker
(ngModelChange)="changeDatetime($event)"
[(ngModel)]="time"
@@ -35,15 +34,15 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
</ngb-timepicker>
</div>
<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>
`,
})
export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
{
public time : NgbTimeStruct;
public date : NgbDateStruct;
public datetime : Date = new Date()
public time : NgbTimeStruct | null = null;
public date : NgbDateStruct | null = null;
public datetime : Date | null = null;
constructor() {
@@ -55,12 +54,21 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
ngOnInit() {
if (this.formControl.value === undefined) {
this.changeDatetime({});
} else {
this.datetime = new Date(this.formControl.value);
this.date = this.getDateStruct(this.datetime);
}
this.formControl.valueChanges.subscribe(value => {
this.datetime = new Date(value)
this.date = this.getDateStruct(this.datetime);
this.time = this.getTimeStruct(this.datetime);
if (value) {
this.datetime = new Date(value);
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) {
this.datetime.setFullYear(this.date.year)
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)
if (this.date && this.time) {
if (!this.datetime) {
this.datetime = new Date();
}
this.datetime.setFullYear(this.date.year)
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(
formatDate(this.datetime, 'YYYY-MM-ddTHH:mm:ss.SSS', 'EN_US', 'CET')
)
this.formControl.setValue(
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();
} 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";
@Component({
selector: 'formly-richtext-type',
template: `
@@ -42,11 +43,20 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
statusbar: false,
autoresize_bottom_margin: 0,
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 = {
plugins: 'lists image imagetools table code searchreplace autoresize',
plugins: 'lists image imagetools table code searchreplace paste autoresize',
menubar: 'edit insert format tools table',
menu: {
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
@@ -59,7 +69,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
}
init_singleline = {
plugins: 'autoresize',
plugins: 'paste autoresize',
menubar: '',
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)};
}
@@ -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">
<head>
<meta charset="utf-8">
<title>App</title>
<title>Cooper, Hillman & Toshi</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<body>
<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 {
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;
}
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/ {
proxy_pass http://docker-back/;
proxy_redirect off;