Compare commits
99 Commits
feature/pd
...
1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2948e9b961 | |||
| 191a3d0018 | |||
| 8885969a07 | |||
| a997e54891 | |||
| 0b12f8718a | |||
| d5d2f31178 | |||
| 1bd2774cdd | |||
| e5375edda8 | |||
| ae306bb89e | |||
| e877c36a3d | |||
| 7f639fc5a0 | |||
| 7fbba953cd | |||
| 4e52999b5d | |||
| 392d66b03e | |||
| dc6616bba6 | |||
| f3f0ddc004 | |||
| 0b9d5d42cb | |||
| bb0ecb5c13 | |||
| 5a5f1b3519 | |||
| 2adecb99d2 | |||
| 3db7c62b09 | |||
| 78c4ee119a | |||
| 46ac3295e3 | |||
| 2349f4c804 | |||
| 334150bc0f | |||
|
|
da19ef652e | ||
|
|
015fc00f6f | ||
| e22c197d3a | |||
| becab58c73 | |||
| 70a863f6e1 | |||
| 2b55d206e2 | |||
| f35182233b | |||
| 43474c960f | |||
|
|
a16a122713 | ||
| b3566b39b8 | |||
| 7bebc05e08 | |||
| 6b49b688ac | |||
| 7dbe4a1716 | |||
| 701ac8e1dc | |||
| 2ba34a675d | |||
| 08cb2772ea | |||
| 86bcb87427 | |||
| 4be6591e81 | |||
| b4f81431b9 | |||
| b1d0e115f4 | |||
| 8aac5376df | |||
| 918ad94861 | |||
| b598e7a147 | |||
| 54082700e8 | |||
| 4a4fca0a40 | |||
| 45116fbb8c | |||
| 2b784850c7 | |||
| 427ea64f8d | |||
| 868eea79bf | |||
| 3390acbc82 | |||
| 8319fa9fac | |||
| ad19a50346 | |||
| f2ddc4303e | |||
| 598a962aba | |||
| 1753271ac0 | |||
| 2cb5af904e | |||
| 1d3918db87 | |||
| a52435d443 | |||
| 1e3855ced5 | |||
| d313f5a3d8 | |||
| 0f1c910919 | |||
| 0011b62a5d | |||
| 5605ee9497 | |||
| eaa79c3541 | |||
| bf536fe8f7 | |||
| ac3268e6c8 | |||
| 9da0063812 | |||
| b7880dc304 | |||
| 374f90b3ec | |||
| 3ecfb7fa3b | |||
| f94fdcd141 | |||
| 0d484209f6 | |||
| 52ea9dfb63 | |||
| 4b00ef12aa | |||
| 34cb9bece4 | |||
| d43164c93d | |||
| 8bb3d0f44e | |||
| 9035824248 | |||
| ab0111c1f5 | |||
| 02e2685c50 | |||
| 62bbe706ca | |||
| 576b5970a5 | |||
| d8c8ebdc48 | |||
| ac34dd1663 | |||
| ac981d18fc | |||
| da634c59ee | |||
| 4d31497b8b | |||
| 5322756e5a | |||
| 9621f90e25 | |||
| 5773179c0d | |||
| 35e532449b | |||
| 6d6dc2d82b | |||
| 61b8e3fe21 | |||
| ab6490622a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
__pycache__/
|
||||
|
||||
back/app/fixtures/
|
||||
back/media/
|
||||
|
||||
front/app-back/
|
||||
front/app-back2/
|
||||
|
||||
12
Makefile
Normal file
12
Makefile
Normal 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 -
|
||||
@@ -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
15
back/Dockerfile.prod
Normal 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"]
|
||||
@@ -1,9 +1,107 @@
|
||||
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, SignatureRead
|
||||
|
||||
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) -> SignatureRead:
|
||||
contract = await Contract.find_by_signature_id(signature_id)
|
||||
signature = contract.get_signature(signature_id)
|
||||
signature_dict = signature.dict()
|
||||
signature_dict['contract_label'] = contract.label
|
||||
return signature_dict
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,81 @@ 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.name)
|
||||
|
||||
|
||||
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}'
|
||||
|
||||
@@ -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)
|
||||
@@ -1,30 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{% include 'styles.css' %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h2>Conditions générales & particuliè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>
|
||||
@@ -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>
|
||||
@@ -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 }} à {{ draft.location}}</p>
|
||||
<p>Entre les soussigné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été de {{ party.entity.entity_data.activity }} enregistrée auprès du gouvernement de San Andreas et domiciliée au {{ party.entity.address }}{% if party.representative %}, représenté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é le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} à {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
|
||||
{% if party.entity.address %} résidant à {{ party.entity.address }}, {% endif %}
|
||||
{% elif party.entity.entity_data.type == "institution" %}
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Ci-après dénommé <strong>{{ party.part|safe }}</strong></p>
|
||||
{% if loop.first %}
|
||||
<p class="part">d'une part</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="part">d'autre part</p>
|
||||
<p>Sous la supervision légale de Maître <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
|
||||
<p>Il a été convenu l'exécution des prestations ci-dessous, conformément aux conditions générales et particulières ci-après:</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 }} à {{ draft.location}}</p>
|
||||
<p>Le {{ contract.date.strftime('%d/%m/%Y') }} à {{ contract.location}}</p>
|
||||
<p>Entre les soussigné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été de {{ party.entity.entity_data.activity }} enregistrée auprès du gouvernement de San Andreas et domiciliée au {{ party.entity.address }}{% if party.representative %}, représenté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é le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if true %} à {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
|
||||
{% if party.entity.entity_data.day_of_birth %} né le {{ party.entity.entity_data.day_of_birth.strftime('%d/%m/%Y') }} {% if party.entity.entity_data.place_of_birth %} à {{ party.entity.entity_data.place_of_birth }}{% endif %},{% endif %}
|
||||
{% if party.entity.address %} résidant à {{ party.entity.address }}, {% endif %}
|
||||
{% elif party.entity.entity_data.type == "institution" %}
|
||||
|
||||
@@ -40,14 +40,14 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="part">d'autre part</p>
|
||||
<p>Sous la supervision légale de Maître <strong>{{ lawyer.firstname }} {{ lawyer.lastname }}</strong></p>
|
||||
<p>Sous la supervision légale de Maître <strong>{{ contract.lawyer.entity_data.firstname }} {{ contract.lawyer.entity_data.lastname }}</strong></p>
|
||||
<p>Il a été convenu l'exécution des prestations ci-dessous, conformément aux conditions générales et particulières ci-après:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Conditions générales & particuliè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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,47 @@ 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
|
||||
|
||||
|
||||
class SignatureRead(BaseModel):
|
||||
signature_uuid: str
|
||||
entity: Entity
|
||||
part: str
|
||||
representative: Entity = None
|
||||
signature_affixed: bool
|
||||
contract_label: str
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
0
back/media/contracts/.gitkeep
Normal file
0
back/media/contracts/.gitkeep
Normal file
0
back/media/signatures/.gitkeep
Normal file
0
back/media/signatures/.gitkeep
Normal 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
27
docker-compose.prod.yml
Normal 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:
|
||||
42
docker-compose.prod.yml.back
Normal file
42
docker-compose.prod.yml.back
Normal 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:
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
17
front/Dockerfile.prod
Normal 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/
|
||||
@@ -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": {
|
||||
|
||||
945
front/app/package-lock.json
generated
945
front/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.sidenav {
|
||||
background-image: url("/assets/leather_texture.png");
|
||||
background-size: 220px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
126
front/app/src/app/layout/auth/auth.component.ts
Normal file
126
front/app/src/app/layout/auth/auth.component.ts
Normal 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()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
38
front/app/src/app/layout/auth/auth.interceptor.ts
Normal file
38
front/app/src/app/layout/auth/auth.interceptor.ts
Normal 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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
75
front/app/src/app/layout/auth/auth.service.ts
Normal file
75
front/app/src/app/layout/auth/auth.service.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.logout {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 Templates",
|
||||
title: $localize`Provision Templates`,
|
||||
link: "/templates/provisions",
|
||||
icon: IconNamesEnum.BlockquoteLeft
|
||||
},
|
||||
{
|
||||
title: "Contracts Templates",
|
||||
title: $localize`Contracts Templates`,
|
||||
link: "/templates/contracts",
|
||||
icon: IconNamesEnum.FileCodeFill
|
||||
},
|
||||
{
|
||||
title: "Contracts Drafts",
|
||||
title: $localize`Contracts 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -9,6 +9,7 @@ export class BaseCrudListComponent {
|
||||
@Input() resource: string = "";
|
||||
@Input() columns: string[] = [];
|
||||
@Input() schema: string | undefined;
|
||||
@Input() filters: string[] = [];
|
||||
|
||||
constructor(
|
||||
public flashService: FlashmessagesService
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<base-card
|
||||
[resource]="this.resource"
|
||||
[schema]="this.schema">
|
||||
</base-card>'
|
||||
</base-card>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<base-list
|
||||
[resource]="this.resource"
|
||||
[schema]="this.schema"
|
||||
[columns]="this.columns">
|
||||
[columns]="this.columns"
|
||||
[filters]="this.filters">
|
||||
</base-list>
|
||||
@@ -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 {}
|
||||
@@ -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 {
|
||||
}
|
||||
173
front/app/src/app/views/contract-drafts/drafts.component.ts
Normal file
173
front/app/src/app/views/contract-drafts/drafts.component.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
168
front/app/src/app/views/contracts/contracts.component.ts
Normal file
168
front/app/src/app/views/contracts/contracts.component.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Component, ElementRef, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from "@angular/router";
|
||||
import { DomSanitizer, Meta } 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 [innerHTML]="this.legalText"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
`
|
||||
})
|
||||
export class ContractsSignatureComponent implements OnInit {
|
||||
signature_id: string | null = null;
|
||||
signature: any = {}
|
||||
signatory = ""
|
||||
|
||||
affixed = false;
|
||||
|
||||
public legalText: string = "";
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private sanitizer: DomSanitizer,
|
||||
private crudService: ImageUploaderCrudService,
|
||||
private meta: Meta,
|
||||
) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.legalText = `
|
||||
<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>
|
||||
`
|
||||
|
||||
this.meta.updateTag({
|
||||
name: 'og:title',
|
||||
property: 'og:title',
|
||||
content: this.signature.contract_label});
|
||||
this.meta.updateTag({
|
||||
name: 'og:description',
|
||||
property: 'og:description',
|
||||
content: this.legalText.replace(/<[^>]*>/g, '').trim()
|
||||
});
|
||||
this.meta.updateTag({ name: 'og:image', content: `${location.origin}/assets/logo.png` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"> </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)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export class BaseEntitiesComponent {
|
||||
})
|
||||
export class EntityListComponent extends BaseEntitiesComponent {
|
||||
columns = ['label', 'address', 'entity_data.type']
|
||||
filters = [];
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
BIN
front/app/src/assets/leather_texture.png
Normal file
BIN
front/app/src/assets/leather_texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
front/app/src/assets/logo.png
Normal file
BIN
front/app/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
99
front/app/src/common/crud/list/filter-list.component.ts
Normal file
99
front/app/src/common/crud/list/filter-list.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.table-row-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
})
|
||||
|
||||
@@ -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('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
24
front/app/src/common/crud/types/signature-link.type.ts
Normal file
24
front/app/src/common/crud/types/signature-link.type.ts
Normal 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
|
||||
}
|
||||
}
|
||||
422
front/app/src/locale/messages.fr.xlf
Normal file
422
front/app/src/locale/messages.fr.xlf
Normal 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&nbsp;Templates</source>
|
||||
<target>Templates:&nbsp;Clauses</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8996893897509995689">
|
||||
<segment state="initial">
|
||||
<source>Contracts&nbsp;Templates</source>
|
||||
<target>Templates:&nbsp;Contrats</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="4480519554689195945">
|
||||
<segment state="initial">
|
||||
<source>Contracts&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="<p>" dispEnd="</p>">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
|
||||
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Si vous n'êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">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="<strong>" dispEnd="</strong>">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="<p>" dispEnd="</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 <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">usurpation d'identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</pc>
|
||||
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Le cabinet Cooper, Hillman & Toshi LLC</pc>
|
||||
</source>
|
||||
<target>
|
||||
<pc id="0" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
|
||||
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Si vous n'êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">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="<strong>" dispEnd="</strong>">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="<p>" dispEnd="</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 <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">usurpation d'identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</pc>
|
||||
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Le cabinet Cooper, Hillman & 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>
|
||||
346
front/app/src/locale/messages.xlf
Normal file
346
front/app/src/locale/messages.xlf
Normal 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&nbsp;Templates</source>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="8996893897509995689">
|
||||
<segment>
|
||||
<source>Contracts&nbsp;Templates</source>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="4480519554689195945">
|
||||
<segment>
|
||||
<source>Contracts&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="<p>" dispEnd="</p>">Cette page est à la destination exclusive de <pc id="1" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>"><ph id="2" equiv="INTERPOLATION" disp="{{ this.signatory }}"/></pc></pc>
|
||||
<pc id="3" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Si vous n'êtes <pc id="4" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">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="<strong>" dispEnd="</strong>">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="<p>" dispEnd="</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 <pc id="8" equivStart="START_TAG_STRONG" equivEnd="CLOSE_TAG_STRONG" type="other" dispStart="<strong>" dispEnd="</strong>">usurpation d'identité</pc> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</pc>
|
||||
<pc id="9" equivStart="START_PARAGRAPH" equivEnd="CLOSE_PARAGRAPH" type="other" dispStart="<p>" dispEnd="</p>">Le cabinet Cooper, Hillman & Toshi LLC</pc>
|
||||
</source>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2990108023996257960">
|
||||
<segment>
|
||||
<source>This Contract has already been signed by</source>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -26,3 +26,14 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
53
front/nginx.prod.conf
Normal file
53
front/nginx.prod.conf
Normal file
@@ -0,0 +1,53 @@
|
||||
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 /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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user