Compare commits
115 Commits
feature/pd
...
1a247f14ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a247f14ce | |||
| c7335af514 | |||
| 706b3dc275 | |||
| ff78f9da54 | |||
| 3a14528402 | |||
| 5c276faf78 | |||
| 95b17947b2 | |||
| f20635e10e | |||
| 8d4d64076a | |||
| f9867b3e2b | |||
| b89d043880 | |||
| 0ecbb99423 | |||
| 61bd5589ff | |||
| 72065c0d0d | |||
| cd8bb58dfb | |||
| 9ab1571067 | |||
| 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__/
|
__pycache__/
|
||||||
|
|
||||||
back/app/fixtures/
|
back/app/fixtures/
|
||||||
|
back/media/
|
||||||
|
|
||||||
front/app-back/
|
front/app-back/
|
||||||
front/app-back2/
|
front/app-back2/
|
||||||
|
|||||||
12
Makefile
Normal file
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 -
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
FROM python:3.10
|
FROM python:3.13
|
||||||
|
|
||||||
RUN apt update && apt install -y xfonts-base xfonts-75dpi python3-pip python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0 \
|
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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
|
|
||||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
|
||||||
COPY ./requirements.txt /code/requirements.txt
|
COPY ./requirements.txt /code/requirements.txt
|
||||||
|
|
||||||
# install project dependencies
|
|
||||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||||
|
|
||||||
# copy project files and folders to the current working directory (i.e. 'app' folder)
|
|
||||||
COPY ./app /code/app
|
COPY ./app /code/app
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|||||||
15
back/Dockerfile.prod
Normal file
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,105 @@
|
|||||||
from fastapi import APIRouter
|
import uuid
|
||||||
|
from fastapi import Depends, HTTPException, File, UploadFile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from ..core.routes import get_crud_router
|
||||||
from .routes_draft import draft_router
|
from .routes_draft import draft_router
|
||||||
from .print import print_router
|
from .print import print_router
|
||||||
|
|
||||||
contract_router = APIRouter()
|
from .models import Contract, ContractDraft, ContractDraftStatus, Party, replace_variables_in_value
|
||||||
|
from .schemas import ContractCreate, ContractRead, ContractUpdate
|
||||||
|
|
||||||
contract_router.include_router(draft_router, prefix="/draft", tags=["draft"], )
|
from ..entity.models import Entity
|
||||||
contract_router.include_router(print_router, prefix="/print", tags=["print"], )
|
from ..template.models import ProvisionTemplate
|
||||||
|
from ..user.manager import get_current_user, get_current_superuser
|
||||||
|
|
||||||
|
contract_router = get_crud_router(Contract, ContractCreate, ContractRead, ContractUpdate)
|
||||||
|
del(contract_router.routes[0])
|
||||||
|
del(contract_router.routes[2])
|
||||||
|
del(contract_router.routes[2])
|
||||||
|
|
||||||
|
contract_router.include_router(draft_router, prefix="/draft", )
|
||||||
|
contract_router.include_router(print_router, prefix="/print", )
|
||||||
|
|
||||||
|
|
||||||
|
@contract_router.post("/", response_description="Contract Successfully created")
|
||||||
|
async def create(item: ContractCreate, user=Depends(get_current_user)) -> dict:
|
||||||
|
await item.validate_foreign_key()
|
||||||
|
|
||||||
|
draft = await ContractDraft.get(item.draft_id)
|
||||||
|
|
||||||
|
for v in draft.variables:
|
||||||
|
if not v.key or not v.value:
|
||||||
|
raise HTTPException(status_code=400, detail="Variable {} is invalid".format(v))
|
||||||
|
|
||||||
|
contract_dict = item.dict()
|
||||||
|
del(contract_dict['draft_id'])
|
||||||
|
|
||||||
|
contract_dict['lawyer'] = await Entity.get(user.entity_id)
|
||||||
|
|
||||||
|
contract_dict['name'] = draft.name
|
||||||
|
contract_dict['title'] = draft.title
|
||||||
|
parties = []
|
||||||
|
for p in draft.parties:
|
||||||
|
parties.append({
|
||||||
|
'entity': await Entity.get(p.entity_id),
|
||||||
|
'part': p.part,
|
||||||
|
'representative': await Entity.get(p.representative_id) if p.representative_id else None,
|
||||||
|
'signature_uuid': str(uuid.uuid4())
|
||||||
|
})
|
||||||
|
|
||||||
|
contract_dict['parties'] = parties
|
||||||
|
|
||||||
|
provisions = []
|
||||||
|
for p in draft.provisions:
|
||||||
|
p = p.provision
|
||||||
|
provision = await ProvisionTemplate.get(p.provision_template_id) if p.type == 'template' \
|
||||||
|
else p
|
||||||
|
|
||||||
|
provisions.append({
|
||||||
|
'title': replace_variables_in_value(draft.variables, provision.title),
|
||||||
|
'body': replace_variables_in_value(draft.variables, provision.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
contract_dict['provisions'] = provisions
|
||||||
|
|
||||||
|
o = await Contract(**contract_dict).create()
|
||||||
|
|
||||||
|
await draft.update({"$set": {"status": ContractDraftStatus.published}})
|
||||||
|
return {"message": "Contract Successfully created", "id": o.id}
|
||||||
|
|
||||||
|
|
||||||
|
@contract_router.put("/{id}", response_description="")
|
||||||
|
async def update(id: str, contract_form: ContractUpdate, user=Depends(get_current_superuser)) -> ContractRead:
|
||||||
|
raise HTTPException(status_code=400, detail="No modification on contract")
|
||||||
|
|
||||||
|
|
||||||
|
@contract_router.get("/signature/{signature_id}", response_description="")
|
||||||
|
async def get_signature(signature_id: str) -> Party:
|
||||||
|
contract = await Contract.find_by_signature_id(signature_id)
|
||||||
|
signature = contract.get_signature(signature_id)
|
||||||
|
return signature
|
||||||
|
|
||||||
|
|
||||||
|
@contract_router.post("/signature/{signature_id}", response_description="")
|
||||||
|
async def affix_signature(signature_id: str, signature_file: UploadFile = File(...)) -> bool:
|
||||||
|
contract = await Contract.find_by_signature_id(signature_id)
|
||||||
|
|
||||||
|
signature_index = contract.get_signature_index(signature_id)
|
||||||
|
signature = contract.parties[signature_index]
|
||||||
|
|
||||||
|
if signature.signature_affixed:
|
||||||
|
raise HTTPException(status_code=400, detail="Signature already affixed")
|
||||||
|
|
||||||
|
with open(f'media/signatures/{signature_id}.png', "wb") as buffer:
|
||||||
|
shutil.copyfileobj(signature_file.file, buffer)
|
||||||
|
|
||||||
|
update_query = {"$set": {
|
||||||
|
f'parties.{signature_index}.signature_affixed': True
|
||||||
|
}}
|
||||||
|
signature.signature_affixed = True
|
||||||
|
if contract.is_signed():
|
||||||
|
update_query["$set"]['status'] = 'signed'
|
||||||
|
await contract.update(update_query)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import List, Literal
|
from typing import List, Literal, Optional
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, validator
|
||||||
|
from beanie.operators import ElemMatch
|
||||||
|
|
||||||
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
|
from ..core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
|
||||||
|
from ..entity.models import Entity
|
||||||
|
|
||||||
|
|
||||||
class ContractStatus(str, Enum):
|
class ContractStatus(str, Enum):
|
||||||
new = 'new'
|
published = 'published'
|
||||||
signed = 'signed'
|
signed = 'signed'
|
||||||
in_effect = 'in_effect'
|
printed = 'printed'
|
||||||
executed = 'executed'
|
executed = 'executed'
|
||||||
|
|
||||||
|
|
||||||
class ContractDraftStatus(str, Enum):
|
class ContractDraftStatus(str, Enum):
|
||||||
draft = 'draft'
|
in_progress = 'in_progress'
|
||||||
created = 'created'
|
ready = 'ready'
|
||||||
|
published = 'published'
|
||||||
|
|
||||||
|
|
||||||
class Party(BaseModel):
|
class DraftParty(BaseModel):
|
||||||
entity_id: str = Field(
|
entity_id: str = Field(
|
||||||
foreignKey={
|
foreignKey={
|
||||||
"reference": {
|
"reference": {
|
||||||
"resource": "entity",
|
"resource": "entity",
|
||||||
"schema": "Entity",
|
"schema": "Entity",
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
default="",
|
||||||
|
title="Partie"
|
||||||
)
|
)
|
||||||
part: str
|
part: str = Field(title="Rôle")
|
||||||
representative_id: str = Field(
|
representative_id: str = Field(
|
||||||
foreignKey={
|
foreignKey={
|
||||||
"reference": {
|
"reference": {
|
||||||
@@ -36,14 +41,30 @@ class Party(BaseModel):
|
|||||||
"schema": "Entity",
|
"schema": "Entity",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
default=""
|
default="",
|
||||||
|
title="Représentant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Partie'
|
||||||
|
|
||||||
|
|
||||||
|
class Party(BaseModel):
|
||||||
|
entity: Entity
|
||||||
|
part: str
|
||||||
|
representative: Optional[Entity] = None
|
||||||
|
signature_uuid: str
|
||||||
|
signature_affixed: bool = False
|
||||||
|
signature_png: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ProvisionGenuine(BaseModel):
|
class ProvisionGenuine(BaseModel):
|
||||||
type: Literal['genuine'] = 'genuine'
|
type: Literal['genuine'] = 'genuine'
|
||||||
title: str = RichtextSingleline(props={"parametrized": True})
|
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
|
||||||
body: str = RichtextMultiline(props={"parametrized": True})
|
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Clause personalisée'
|
||||||
|
|
||||||
|
|
||||||
class ContractProvisionTemplateReference(BaseModel):
|
class ContractProvisionTemplateReference(BaseModel):
|
||||||
@@ -56,23 +77,157 @@ class ContractProvisionTemplateReference(BaseModel):
|
|||||||
"displayedFields": ['title', 'body']
|
"displayedFields": ['title', 'body']
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props={"parametrized": True}
|
props={"parametrized": True},
|
||||||
|
default="",
|
||||||
|
title="Template de clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Template de clause'
|
||||||
|
|
||||||
|
|
||||||
class DraftProvision(BaseModel):
|
class DraftProvision(BaseModel):
|
||||||
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
|
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Clause'
|
||||||
|
|
||||||
|
|
||||||
|
class Provision(BaseModel):
|
||||||
|
title: str = RichtextSingleline(title="Titre")
|
||||||
|
body: str = RichtextMultiline(title="Corps")
|
||||||
|
|
||||||
|
|
||||||
class ContractDraft(CrudDocument):
|
class ContractDraft(CrudDocument):
|
||||||
name: str
|
"""
|
||||||
title: str
|
Brouillon de contrat à remplir
|
||||||
parties: List[Party]
|
"""
|
||||||
provisions: List[DraftProvision]
|
|
||||||
|
name: str = Field(title="Nom")
|
||||||
|
title: str = Field(title="Titre")
|
||||||
|
parties: List[DraftParty] = Field(title="Parties")
|
||||||
|
provisions: List[DraftProvision] = Field(
|
||||||
|
props={"items-per-row": "1", "numbered": True},
|
||||||
|
title='Clauses'
|
||||||
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
format="dictionary",
|
||||||
|
title='Variables'
|
||||||
)
|
)
|
||||||
status: ContractDraftStatus = Field(default=ContractDraftStatus.draft)
|
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
|
||||||
location: str = ""
|
todo: List[str] = Field(default=[], title="Reste à faire")
|
||||||
date: datetime.date = datetime.date(1970, 1, 1)
|
|
||||||
|
class Settings(CrudDocument.Settings):
|
||||||
|
fulltext_search = ['name', 'title']
|
||||||
|
|
||||||
|
bson_encoders = {
|
||||||
|
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
|
||||||
|
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
|
||||||
|
hour=0, minute=0, second=0)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Brouillon de contrat'
|
||||||
|
|
||||||
|
async def check_is_ready(self):
|
||||||
|
if self.status == ContractDraftStatus.published:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.todo = []
|
||||||
|
if len(self.parties) < 2:
|
||||||
|
self.todo.append('Contract must have at least two parties')
|
||||||
|
if len(self.provisions) < 1:
|
||||||
|
self.todo.append('Contract must have at least one provision')
|
||||||
|
|
||||||
|
for p in self.parties:
|
||||||
|
if not p.entity_id:
|
||||||
|
self.todo.append('All parties must have an associated entity`')
|
||||||
|
|
||||||
|
for p in self.provisions:
|
||||||
|
if p.provision.type == "genuine" and not (p.provision.title and p.provision.body):
|
||||||
|
self.todo.append('Empty genuine provision')
|
||||||
|
elif p.provision.type == "template" and not p.provision.provision_template_id:
|
||||||
|
self.todo.append('Empty template provision')
|
||||||
|
|
||||||
|
for v in self.variables:
|
||||||
|
if not (v.key and v.value):
|
||||||
|
self.todo.append('Empty variable')
|
||||||
|
|
||||||
|
if self.todo:
|
||||||
|
self.status = ContractDraftStatus.in_progress
|
||||||
|
else:
|
||||||
|
self.status = ContractDraftStatus.ready
|
||||||
|
|
||||||
|
await self.update({"$set": {
|
||||||
|
"status": self.status,
|
||||||
|
"todo": self.todo
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
class Contract(CrudDocument):
|
||||||
|
"""
|
||||||
|
Contrat publié. Les contrats ne peuvent pas être modifiés.
|
||||||
|
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
|
||||||
|
"""
|
||||||
|
name: str = Field(title="Nom")
|
||||||
|
title: str = Field(title="Titre")
|
||||||
|
parties: List[Party] = Field(title="Parties")
|
||||||
|
provisions: List[Provision] = Field(
|
||||||
|
props={"items-per-row": "1", "numbered": True},
|
||||||
|
title='Clauses'
|
||||||
|
)
|
||||||
|
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
|
||||||
|
lawyer: Entity = Field(title="Avocat en charge")
|
||||||
|
location: str = Field(title="Lieu")
|
||||||
|
date: datetime.date = Field(title="Date")
|
||||||
|
label: Optional[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.responses import HTMLResponse, FileResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
@@ -9,7 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from app.entity.models import Entity
|
from app.entity.models import Entity
|
||||||
from app.template.models import ProvisionTemplate
|
from app.template.models import ProvisionTemplate
|
||||||
from ..schemas import ContractDraft
|
from ..models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
|
||||||
|
|
||||||
|
|
||||||
async def build_model(model):
|
async def build_model(model):
|
||||||
@@ -24,19 +28,28 @@ async def build_model(model):
|
|||||||
|
|
||||||
parties.append(party)
|
parties.append(party)
|
||||||
|
|
||||||
|
|
||||||
model.parties = parties
|
model.parties = parties
|
||||||
|
|
||||||
provisions = []
|
provisions = []
|
||||||
for p in model.provisions:
|
for p in model.provisions:
|
||||||
if p.provision.type == "template":
|
if p.provision.type == "template":
|
||||||
provisions.append(await ProvisionTemplate.get(p.provision.provision_template_id))
|
provision = await ProvisionTemplate.get(p.provision.provision_template_id)
|
||||||
else:
|
else:
|
||||||
provisions.append(p.provision)
|
provision = p.provision
|
||||||
|
|
||||||
|
provision.title = replace_variables_in_value(model.variables, provision.title)
|
||||||
|
provision.body = replace_variables_in_value(model.variables, provision.body)
|
||||||
|
provisions.append(provision)
|
||||||
|
|
||||||
model.provisions = provisions
|
model.provisions = provisions
|
||||||
|
|
||||||
model.location = "Toulouse"
|
model = model.dict()
|
||||||
model.date = "01/01/1970"
|
model['location'] = "Los Santos, SA"
|
||||||
|
model['date'] = datetime.date(1970, 1, 1)
|
||||||
|
model['lawyer'] = {'entity_data': {
|
||||||
|
"firstname": "prénom avocat",
|
||||||
|
"lastname": "nom avocat",
|
||||||
|
}}
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
@@ -48,49 +61,96 @@ print_router = APIRouter()
|
|||||||
templates = Jinja2Templates(directory=str(BASE_PATH / "templates"))
|
templates = Jinja2Templates(directory=str(BASE_PATH / "templates"))
|
||||||
|
|
||||||
|
|
||||||
async def render_print(host, draft, lawyer):
|
async def render_print(root_url, contract):
|
||||||
template = templates.get_template("print.html")
|
template = templates.get_template("print.html")
|
||||||
return template.render({
|
return template.render({
|
||||||
"draft": draft,
|
"contract": contract,
|
||||||
"lawyer": lawyer,
|
"root_url": root_url
|
||||||
"static_host": host
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
async def render_css(host, draft):
|
async def render_css(root_url, contract):
|
||||||
template = templates.get_template("styles.css")
|
template = templates.get_template("styles.css")
|
||||||
return template.render({
|
return template.render({
|
||||||
"draft": draft,
|
"contract": contract,
|
||||||
"static_host": host
|
"root_url": root_url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@print_router.get("/", response_class=HTMLResponse)
|
@print_router.get("/preview/draft/{draft_id}", response_class=HTMLResponse)
|
||||||
async def create() -> str:
|
async def preview_draft(draft_id: str, request: Request) -> str:
|
||||||
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
|
draft = await build_model(await ContractDraft.get(draft_id))
|
||||||
lawyer = {
|
|
||||||
"firstname": "Nathaniel",
|
|
||||||
"lastname": "Toshi",
|
|
||||||
}
|
|
||||||
|
|
||||||
return await render_print('localhost', draft, lawyer)
|
return await render_print('', draft)
|
||||||
|
|
||||||
|
|
||||||
@print_router.get("/pdf", response_class=FileResponse)
|
@print_router.get("/preview/signature/{signature_id}", response_class=HTMLResponse)
|
||||||
async def create_pdf() -> str:
|
async def preview_contract_by_signature(signature_id: str, request: Request) -> str:
|
||||||
draft = await build_model(await ContractDraft.get("63e92534aafed8b509f229c4"))
|
contract = await Contract.find_by_signature_id(signature_id)
|
||||||
lawyer = {
|
for p in contract.parties:
|
||||||
"firstname": "Nathaniel",
|
if p.signature_affixed:
|
||||||
"lastname": "Toshi",
|
p.signature_png = retrieve_signature_png(f'media/signatures/{p.signature_uuid}.png')
|
||||||
}
|
|
||||||
|
return await render_print('', contract)
|
||||||
|
|
||||||
|
|
||||||
|
@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()
|
font_config = FontConfiguration()
|
||||||
html = HTML(string=await render_print('nginx', draft, lawyer))
|
html = HTML(string=await render_print('http://nginx', contract))
|
||||||
css = CSS(string=await render_css('nginx', draft), font_config=font_config)
|
css = CSS(string=await render_css('http://nginx', contract), font_config=font_config)
|
||||||
|
|
||||||
html.write_pdf('out.pdf', stylesheets=[css], font_config=font_config)
|
html.write_pdf(contract_path, stylesheets=[css], font_config=font_config)
|
||||||
|
update_query = {"$set": {
|
||||||
|
'status': 'printed'
|
||||||
|
}}
|
||||||
|
await contract.update(update_query)
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
"out.pdf",
|
contract_path,
|
||||||
media_type="application/pdf",
|
media_type="application/pdf",
|
||||||
filename=draft.name)
|
filename=contract.label)
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_signature_png(filepath):
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
b_content = f.read()
|
||||||
|
base64_utf8_str = base64.b64encode(b_content).decode('utf-8')
|
||||||
|
ext = filepath.split('.')[-1]
|
||||||
|
return f'data:image/{ext};base64,{base64_utf8_str}'
|
||||||
|
|
||||||
|
|
||||||
|
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse)
|
||||||
|
async def get_signature_opengraph(signature_id: str, request: Request) -> str:
|
||||||
|
contract = await Contract.find_by_signature_id(signature_id)
|
||||||
|
signature = contract.get_signature(signature_id)
|
||||||
|
template = templates.get_template("opengraph.html")
|
||||||
|
|
||||||
|
signatory = signature.representative.label if signature.representative else signature.entity.label
|
||||||
|
|
||||||
|
return template.render({
|
||||||
|
"signatory": signatory,
|
||||||
|
"title": contract.label,
|
||||||
|
"origin_url": f"{request.url.scheme}://{request.url.hostname}"
|
||||||
|
})
|
||||||
|
|||||||
@@ -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>
|
|
||||||
10
back/app/contract/print/templates/opengraph.html
Normal file
10
back/app/contract/print/templates/opengraph.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:title" content="{{ title }}" />
|
||||||
|
<meta property="og:description" content="Cette page est à la destination exclusive de {{ signatory }}
|
||||||
|
Si vous n'êtes pas {{ signatory }}, veuillez fermer cette page immédiatement et surpprimer tous les liens en votre possession menant vers celle-ci.
|
||||||
|
En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour usurpation d'identité et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.
|
||||||
|
Le cabinet Cooper, Hillman & Toshi LLC" />
|
||||||
|
<meta property="og:image" content="{{ origin_url }}/assets/logo.png" />
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
@@ -8,16 +8,16 @@
|
|||||||
<div class="frontpage">
|
<div class="frontpage">
|
||||||
<div id="front-page-header">
|
<div id="front-page-header">
|
||||||
<table><tr>
|
<table><tr>
|
||||||
<td><img id="top-logo" src="http://{{ static_host }}/assets/logotransparent.png" alt="Cooper, Hillman & Toshi logo"></td>
|
<td><img id="top-logo" src="{{ root_url }}/assets/logotransparent.png" alt="Cooper, Hillman & Toshi logo"></td>
|
||||||
<td id="office-info">Cooper, Hillman & Toshi LLP<br />6834 Innocence Boulevard<br />LOS SANTOS - SA<br /><a href="#">consulting@cht.law.com</a></td>
|
<td id="office-info">Cooper, Hillman & Toshi LLP<br />6834 Innocence Boulevard<br />LOS SANTOS - SA<br /><a href="#">consulting@cht.law.com</a></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
<h1>{{ draft.title|upper }}</h1>
|
<h1>{{ contract.title|upper }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
<h2>Introduction</h2>
|
<h2>Introduction</h2>
|
||||||
<p>Le {{ draft.date }} à {{ draft.location}}</p>
|
<p>Le {{ contract.date.strftime('%d/%m/%Y') }} à {{ contract.location}}</p>
|
||||||
<p>Entre les soussignés :</p>
|
<p>Entre les soussignés :</p>
|
||||||
{% for party in draft.parties %}
|
{% for party in contract.parties %}
|
||||||
<div class="party">
|
<div class="party">
|
||||||
{% if not loop.first %}
|
{% if not loop.first %}
|
||||||
<p>ET</p>
|
<p>ET</p>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
{{ party.entity.entity_data.title }} socié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 %}
|
{{ 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" %}
|
{% elif party.entity.entity_data.type == "individual" %}
|
||||||
{{ party.entity.entity_data.firstname }} {{ party.entity.entity_data.middlenames }} {{ party.entity.entity_data.lastname }}
|
{{ party.entity.entity_data.firstname }} {{ party.entity.entity_data.middlenames }} {{ party.entity.entity_data.lastname }}
|
||||||
{% if party.entity.entity_data.day_of_birth %} né 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 %}
|
{% if party.entity.address %} résidant à {{ party.entity.address }}, {% endif %}
|
||||||
{% elif party.entity.entity_data.type == "institution" %}
|
{% elif party.entity.entity_data.type == "institution" %}
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<p class="part">d'autre part</p>
|
<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>
|
<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>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>Conditions générales & particulières</h2>
|
<h2>Conditions générales & particulières</h2>
|
||||||
|
|
||||||
{% for provision in draft.provisions %}
|
{% for provision in contract.provisions %}
|
||||||
<div class="provision">
|
<div class="provision">
|
||||||
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
|
<h3>Article {{loop.index}} - {{ provision.title|safe }}</h3>
|
||||||
<p>{{ provision.body|safe }}</p>
|
<p>{{ provision.body|safe }}</p>
|
||||||
@@ -56,11 +56,18 @@
|
|||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<hr/>
|
<hr/>
|
||||||
<p>À {{ draft.location }} le {{ draft.date }}</p>
|
<p>À {{ contract.location }} le {{ contract.date.strftime('%d/%m/%Y') }}</p>
|
||||||
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p>
|
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p>
|
||||||
<table class="signatures">
|
<table class="signatures">
|
||||||
<tr>
|
<tr>
|
||||||
{% for party in draft.parties %}<td>{{ party.part|safe }}:</td>{% endfor %}
|
{% for party in contract.parties %}
|
||||||
|
<td>
|
||||||
|
{{ party.part|safe }}:<br/>
|
||||||
|
{% if party.signature_png %}
|
||||||
|
<img src="{{ party.signature_png }}" />
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Century Schoolbook';
|
font-family: 'Century Schoolbook';
|
||||||
src: url('http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf');
|
src: url('{{ root_url }}/assets/century-schoolbook/CenturySchoolbookRegular.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Century Schoolbook";
|
font-family: "Century Schoolbook";
|
||||||
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBold.ttf");
|
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBold.ttf");
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Century Schoolbook";
|
font-family: "Century Schoolbook";
|
||||||
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf");
|
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookItalic.ttf");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Century Schoolbook";
|
font-family: "Century Schoolbook";
|
||||||
src: url("http://{{ static_host }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf");
|
src: url("{{ root_url }}/assets/century-schoolbook/CenturySchoolbookBoldItalic.ttf");
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,10 @@
|
|||||||
margin: 2cm 2cm 2cm 2cm;
|
margin: 2cm 2cm 2cm 2cm;
|
||||||
counter-increment: page;
|
counter-increment: page;
|
||||||
@bottom-center {
|
@bottom-center {
|
||||||
content: "© Cooper, Hillman & Toshi LLC - {{ draft.name }} - Page " counter(page) "/" counter(pages);
|
content: "© Cooper, Hillman & Toshi LLC - {{ contract.name }} - Page " counter(page) "/" counter(pages);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
background: url('http://{{ static_host }}/assets/watermark.png') no-repeat;
|
background: url('{{ root_url }}/assets/watermark.png') no-repeat;
|
||||||
background-size:contain;
|
background-size:contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +109,11 @@ h2 {
|
|||||||
text-indent: 2em;
|
text-indent: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content td p {
|
||||||
|
text-indent: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.provision {
|
.provision {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
@@ -117,13 +122,17 @@ p {
|
|||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention {
|
.mention {
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,47 @@
|
|||||||
from fastapi import APIRouter
|
from beanie import PydanticObjectId
|
||||||
|
from fastapi import HTTPException, Depends
|
||||||
|
|
||||||
from ..core.routes import get_crud_router
|
from ..core.routes import get_crud_router
|
||||||
from .models import ContractDraft
|
from ..user.manager import get_current_user
|
||||||
|
|
||||||
|
from .models import ContractDraft, ContractDraftStatus
|
||||||
from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
|
from .schemas import ContractDraftCreate, ContractDraftRead, ContractDraftUpdate
|
||||||
|
|
||||||
draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate)
|
draft_router = get_crud_router(ContractDraft, ContractDraftCreate, ContractDraftRead, ContractDraftUpdate)
|
||||||
|
|
||||||
|
del(draft_router.routes[0])
|
||||||
|
del(draft_router.routes[2])
|
||||||
|
|
||||||
|
|
||||||
|
@draft_router.post("/", response_description="Contract Draft added to the database")
|
||||||
|
async def create(item: ContractDraftCreate, user=Depends(get_current_user)) -> dict:
|
||||||
|
await item.validate_foreign_key()
|
||||||
|
o = await ContractDraft(**item.dict()).create()
|
||||||
|
await o.check_is_ready()
|
||||||
|
|
||||||
|
return {"message": "Contract Draft added successfully", "id": o.id}
|
||||||
|
|
||||||
|
|
||||||
|
@draft_router.put("/{id}", response_description="Contract Draft record updated")
|
||||||
|
async def update(id: PydanticObjectId, req: ContractDraftUpdate, user=Depends(get_current_user)) -> ContractDraftRead:
|
||||||
|
req = {k: v for k, v in req.dict().items() if v is not None}
|
||||||
|
update_query = {"$set": {
|
||||||
|
field: value for field, value in req.items()
|
||||||
|
}}
|
||||||
|
|
||||||
|
item = await ContractDraft.get(id)
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Contract Draft record not found!"
|
||||||
|
)
|
||||||
|
if item.status == ContractDraftStatus.published:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Contract Draft has already been published"
|
||||||
|
)
|
||||||
|
|
||||||
|
await item.update(update_query)
|
||||||
|
await item.check_is_ready()
|
||||||
|
|
||||||
|
return ContractDraftRead(**item.dict())
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from .models import ContractDraft, DraftProvision, Party
|
from .models import ContractDraft, DraftProvision, DraftParty, Contract
|
||||||
|
|
||||||
from ..entity.models import Entity
|
from ..entity.models import Entity
|
||||||
from ..core.schemas import Writer
|
from ..core.schemas import Writer
|
||||||
@@ -15,13 +15,17 @@ class ContractDraftRead(ContractDraft):
|
|||||||
|
|
||||||
|
|
||||||
class ContractDraftCreate(Writer):
|
class ContractDraftCreate(Writer):
|
||||||
name: str
|
name: str = Field(title='Nom')
|
||||||
title: str
|
title: str = Field(title='Titre')
|
||||||
parties: List[Party]
|
parties: List[DraftParty] = Field(title='Parties')
|
||||||
provisions: List[DraftProvision]
|
provisions: List[DraftProvision] = Field(
|
||||||
|
props={"items-per-row": "1", "numbered": True},
|
||||||
|
title='Clauses'
|
||||||
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
format="dictionary",
|
||||||
|
title='Variables'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def validate_foreign_key(self):
|
async def validate_foreign_key(self):
|
||||||
@@ -34,3 +38,38 @@ class ContractDraftCreate(Writer):
|
|||||||
|
|
||||||
class ContractDraftUpdate(ContractDraftCreate):
|
class ContractDraftUpdate(ContractDraftCreate):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ForeignEntityRead(BaseModel):
|
||||||
|
label: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = "Avocat"
|
||||||
|
|
||||||
|
|
||||||
|
class PartyRead(BaseModel):
|
||||||
|
signature_affixed: bool = Field(title='Signature apposée?')
|
||||||
|
signature_uuid: str = Field(format="signature-link", title="Lien vers signature")
|
||||||
|
part: str = Field(title='Rôle')
|
||||||
|
entity: ForeignEntityRead = Field(title='Client')
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = "Partie"
|
||||||
|
|
||||||
|
|
||||||
|
class ContractRead(Contract):
|
||||||
|
parties: List[PartyRead]
|
||||||
|
lawyer: ForeignEntityRead
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = "Contrat"
|
||||||
|
|
||||||
|
|
||||||
|
class ContractCreate(Writer):
|
||||||
|
date: datetime.date
|
||||||
|
location: str
|
||||||
|
draft_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class ContractUpdate(BaseModel):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from pydantic import BaseModel, Field, validator
|
|||||||
|
|
||||||
class CrudDocument(Document):
|
class CrudDocument(Document):
|
||||||
_id: str
|
_id: str
|
||||||
created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
|
created_at: datetime = Field(default=datetime.utcnow(), nullable=False, title="Créé le")
|
||||||
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
|
||||||
|
|
||||||
@validator("label", always=True, check_fields=False)
|
@validator("label", always=True, check_fields=False)
|
||||||
def generate_label(cls, v, values, **kwargs):
|
def generate_label(cls, v, values, **kwargs):
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
|
from beanie.odm.operators.find.comparison import In
|
||||||
from beanie.operators import And, RegEx, Eq
|
from beanie.operators import And, RegEx, Eq
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from fastapi_paginate import Page, Params, add_pagination
|
from fastapi_pagination import Page, Params, add_pagination
|
||||||
from fastapi_paginate.ext.motor import paginate
|
from fastapi_pagination.ext.beanie import paginate
|
||||||
|
|
||||||
|
from ..user.manager import get_current_user, get_current_superuser, get_current_user_and_firm
|
||||||
|
|
||||||
|
|
||||||
def parse_sort(sort_by):
|
def parse_sort(sort_by):
|
||||||
@@ -36,16 +39,21 @@ def parse_query(query: str, model):
|
|||||||
|
|
||||||
or_array = []
|
or_array = []
|
||||||
for field in model.Settings.fulltext_search:
|
for field in model.Settings.fulltext_search:
|
||||||
or_array.append(RegEx(field, value, 'i'))
|
words_and_array = []
|
||||||
|
for word in value.split(' '):
|
||||||
|
words_and_array.append(RegEx(field, word, 'i'))
|
||||||
|
or_array.append(And(*words_and_array) if len(words_and_array) > 1 else words_and_array[0])
|
||||||
operand = Or(or_array) if len(or_array) > 1 else or_array[0]
|
operand = Or(or_array) if len(or_array) > 1 else or_array[0]
|
||||||
|
|
||||||
elif operator == 'eq':
|
elif operator == 'eq':
|
||||||
operand = Eq(column, value)
|
operand = Eq(column, value)
|
||||||
|
elif operator == 'in':
|
||||||
|
operand = In(column, value.split(','))
|
||||||
|
|
||||||
and_array.append(operand)
|
and_array.append(operand)
|
||||||
|
|
||||||
if and_array:
|
if and_array:
|
||||||
return And(and_array) if len(and_array) > 1 else and_array[0]
|
return And(*and_array) if len(and_array) > 1 else and_array[0]
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -55,27 +63,27 @@ def get_crud_router(model, model_create, model_read, model_update):
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/", response_description="{} added to the database".format(model.__name__))
|
@router.post("/", response_description="{} added to the database".format(model.__name__))
|
||||||
async def create(item: model_create) -> dict:
|
async def create(instance: str, firm: str, item: model_create, user=Depends(get_current_user)) -> dict:
|
||||||
await item.validate_foreign_key()
|
await item.validate_foreign_key()
|
||||||
o = await model(**item.dict()).create()
|
o = await model(**item.dict()).create()
|
||||||
return {"message": "{} added successfully".format(model.__name__), "id": o.id}
|
return {"message": "{} added successfully".format(model.__name__), "id": o.id}
|
||||||
|
|
||||||
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
|
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
|
||||||
async def read_id(id: PydanticObjectId) -> model_read:
|
async def read_id(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
|
||||||
item = await model.get(id)
|
item = await model.get(id)
|
||||||
return model_read(**item.dict())
|
return model_read(**item.dict())
|
||||||
|
|
||||||
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
|
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
|
||||||
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None) -> Page[model_read]:
|
async def read_list(instance: str, firm: str, size: int = 50, page: int = 1, sort_by: str = None, query: str = None,
|
||||||
|
user=Depends(get_current_user_and_firm)) -> Page[model_read]:
|
||||||
sort = parse_sort(sort_by)
|
sort = parse_sort(sort_by)
|
||||||
query = parse_query(query, model_read)
|
query = parse_query(query, model_read)
|
||||||
|
|
||||||
collection = model.get_motor_collection()
|
items = paginate(model.find(query), Params(**{'size': size, 'page': page}))
|
||||||
items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort)
|
|
||||||
return await items
|
return await items
|
||||||
|
|
||||||
@router.put("/{id}", response_description="{} record updated".format(model.__name__))
|
@router.put("/{id}", response_description="{} record updated".format(model.__name__))
|
||||||
async def update(id: PydanticObjectId, req: model_update) -> model_read:
|
async def update(instance: str, firm: str, id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
|
||||||
req = {k: v for k, v in req.dict().items() if v is not None}
|
req = {k: v for k, v in req.dict().items() if v is not None}
|
||||||
update_query = {"$set": {
|
update_query = {"$set": {
|
||||||
field: value for field, value in req.items()
|
field: value for field, value in req.items()
|
||||||
@@ -92,7 +100,7 @@ def get_crud_router(model, model_create, model_read, model_update):
|
|||||||
return model_read(**item.dict())
|
return model_read(**item.dict())
|
||||||
|
|
||||||
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
|
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
|
||||||
async def delete(id: PydanticObjectId) -> dict:
|
async def delete(instance: str, firm: str, id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
|
||||||
item = await model.get(id)
|
item = await model.get(id)
|
||||||
|
|
||||||
if not item:
|
if not item:
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from beanie import init_beanie
|
|||||||
from .user import User, AccessToken
|
from .user import User, AccessToken
|
||||||
from .entity.models import Entity
|
from .entity.models import Entity
|
||||||
from .template.models import ContractTemplate, ProvisionTemplate
|
from .template.models import ContractTemplate, ProvisionTemplate
|
||||||
from .order.models import Order
|
from .contract.models import ContractDraft, Contract
|
||||||
from .contract.models import ContractDraft
|
|
||||||
|
|
||||||
DATABASE_URL = "mongodb://root:example@mongo:27017/"
|
DB_PASSWORD = "IBO3eber0mdw2R9pnInLdtFykQFY2f06"
|
||||||
|
DATABASE_URL = f"mongodb://root:{DB_PASSWORD}@mongo:27017/"
|
||||||
|
|
||||||
|
|
||||||
async def init_db():
|
async def init_db():
|
||||||
@@ -17,5 +17,6 @@ async def init_db():
|
|||||||
)
|
)
|
||||||
|
|
||||||
await init_beanie(database=client.db_name,
|
await init_beanie(database=client.db_name,
|
||||||
document_models=[User, AccessToken, Entity, ContractTemplate, ProvisionTemplate, ContractDraft, ],
|
document_models=[User, AccessToken, Entity, ContractTemplate, ProvisionTemplate, ContractDraft,
|
||||||
|
Contract, ],
|
||||||
allow_index_dropping=True)
|
allow_index_dropping=True)
|
||||||
|
|||||||
@@ -15,51 +15,68 @@ class EntityType(BaseModel):
|
|||||||
|
|
||||||
class Individual(EntityType):
|
class Individual(EntityType):
|
||||||
type: Literal['individual'] = 'individual'
|
type: Literal['individual'] = 'individual'
|
||||||
firstname: Indexed(str)
|
firstname: Indexed(str) = Field(title='Prénom')
|
||||||
middlename: Indexed(str) = ""
|
middlename: Indexed(str) = Field(default="", title='Autres prénoms')
|
||||||
lastname: Indexed(str)
|
lastname: Indexed(str) = Field(title='Nom de famille')
|
||||||
surnames: List[Indexed(str)] = []
|
surnames: List[Indexed(str)] = Field(
|
||||||
day_of_birth: date
|
default=[],
|
||||||
place_of_birth: str = ""
|
props={"items-per-row": "4", "numbered": True},
|
||||||
|
title="Surnoms"
|
||||||
|
)
|
||||||
|
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
|
||||||
|
place_of_birth: Optional[str] = Field(default="", title='Lieu de naissance')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
if len(self.surnames) > 0:
|
# if len(self.surnames) > 0:
|
||||||
return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
|
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
|
||||||
|
|
||||||
return '{} {}'.format(self.firstname, self.lastname)
|
return '{} {}'.format(self.firstname, self.lastname)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Particulier'
|
||||||
|
|
||||||
|
|
||||||
class Employee(BaseModel):
|
class Employee(BaseModel):
|
||||||
role: Indexed(str)
|
role: Indexed(str) = Field(title='Poste')
|
||||||
entity_id: str = Field(foreignKey={
|
entity_id: str = Field(foreignKey={
|
||||||
"reference": {
|
"reference": {
|
||||||
"resource": "entity",
|
"resource": "entity",
|
||||||
"schema": "Entity",
|
"schema": "Entity",
|
||||||
"condition": "entity_data.type=individual"
|
"condition": "entity_data.type=individual"
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
title='Employé'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Fiche Employé'
|
||||||
|
|
||||||
|
|
||||||
class Corporation(EntityType):
|
class Corporation(EntityType):
|
||||||
type: Literal['corporation'] = 'corporation'
|
type: Literal['corporation'] = 'corporation'
|
||||||
title: Indexed(str)
|
title: Indexed(str) = Field(title='Dénomination sociale')
|
||||||
activity: Indexed(str)
|
activity: Indexed(str) = Field(title='Activité')
|
||||||
employees: List[Employee] = Field(default=[])
|
employees: List[Employee] = Field(default=[], title='Employés')
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Entreprise'
|
||||||
|
|
||||||
|
|
||||||
class Institution(EntityType):
|
class Institution(Corporation):
|
||||||
type: Literal['institution'] = 'institution'
|
type: Literal['institution'] = 'institution'
|
||||||
title: Indexed(str)
|
|
||||||
activity: Indexed(str)
|
class Config:
|
||||||
employees: List[Employee] = Field(default=[])
|
title = 'Institution'
|
||||||
|
|
||||||
|
|
||||||
class Entity(CrudDocument):
|
class Entity(CrudDocument):
|
||||||
|
"""
|
||||||
|
Fiche d'un client
|
||||||
|
"""
|
||||||
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
||||||
label: str = None
|
label: str = None
|
||||||
address: Optional[str] = ""
|
address: str = Field(default="", title='Adresse')
|
||||||
|
|
||||||
@validator("label", always=True)
|
@validator("label", always=True)
|
||||||
def generate_label(cls, v, values, **kwargs):
|
def generate_label(cls, v, values, **kwargs):
|
||||||
@@ -75,3 +92,10 @@ class Entity(CrudDocument):
|
|||||||
else datetime(year=dt.year, month=dt.month, day=dt.day,
|
else datetime(year=dt.year, month=dt.month, day=dt.day,
|
||||||
hour=0, minute=0, second=0)
|
hour=0, minute=0, second=0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Client'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_create_resource(cls):
|
||||||
|
print('coucou')
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ class EntityRead(Entity):
|
|||||||
|
|
||||||
class EntityCreate(Writer):
|
class EntityCreate(Writer):
|
||||||
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
||||||
address: Optional[str] = ""
|
address: str = Field(default="", title='Adresse')
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = "Création d'un client"
|
||||||
|
|
||||||
|
|
||||||
class EntityUpdate(BaseModel):
|
class EntityUpdate(EntityCreate):
|
||||||
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
pass
|
||||||
address: Optional[str] = ""
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from .contract import contract_router
|
|||||||
from .db import init_db
|
from .db import init_db
|
||||||
from .user import user_router, get_auth_router
|
from .user import user_router, get_auth_router
|
||||||
from .entity import entity_router
|
from .entity import entity_router
|
||||||
from .order import order_router
|
|
||||||
from .template import template_router
|
from .template import template_router
|
||||||
|
# from .order import order_router
|
||||||
|
|
||||||
app = FastAPI(root_path="/api/v1")
|
app = FastAPI(root_path="/api/v1")
|
||||||
|
|
||||||
@@ -15,17 +15,14 @@ async def on_startup():
|
|||||||
await init_db()
|
await init_db()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"message": "Hello World"}
|
|
||||||
|
|
||||||
|
|
||||||
app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], )
|
app.include_router(get_auth_router(), prefix="/auth", tags=["auth"], )
|
||||||
app.include_router(user_router, prefix="/users", tags=["users"], )
|
app.include_router(user_router, prefix="/users", tags=["users"], )
|
||||||
app.include_router(entity_router, prefix="/entity", tags=["entity"], )
|
|
||||||
app.include_router(order_router, prefix="/order", tags=["order"], )
|
multitenant_prefix = "/{instance}/{firm}"
|
||||||
app.include_router(template_router, prefix="/template", tags=["template"], )
|
|
||||||
app.include_router(contract_router, prefix="/contract", tags=["contract"], )
|
app.include_router(entity_router, prefix=f"{multitenant_prefix}/entity", tags=["entity"], )
|
||||||
|
app.include_router(template_router, prefix=f"{multitenant_prefix}/template", tags=["template"], )
|
||||||
|
app.include_router(contract_router, prefix=f"{multitenant_prefix}/contract", tags=["contract"], )
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ class PartyTemplate(BaseModel):
|
|||||||
"schema": "Entity",
|
"schema": "Entity",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
default=""
|
default="",
|
||||||
|
title="Partie"
|
||||||
)
|
)
|
||||||
part: str
|
part: str = Field(title="Rôle")
|
||||||
representative_id: str = Field(
|
representative_id: str = Field(
|
||||||
foreignKey={
|
foreignKey={
|
||||||
"reference": {
|
"reference": {
|
||||||
@@ -24,9 +25,13 @@ class PartyTemplate(BaseModel):
|
|||||||
"schema": "Entity",
|
"schema": "Entity",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
default=""
|
default="",
|
||||||
|
title="Représentant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Partie'
|
||||||
|
|
||||||
|
|
||||||
def remove_html_tags(text):
|
def remove_html_tags(text):
|
||||||
"""Remove html tags from a string"""
|
"""Remove html tags from a string"""
|
||||||
@@ -36,10 +41,14 @@ def remove_html_tags(text):
|
|||||||
|
|
||||||
|
|
||||||
class ProvisionTemplate(CrudDocument):
|
class ProvisionTemplate(CrudDocument):
|
||||||
name: str
|
"""
|
||||||
title: str = RichtextSingleline()
|
Modèle de clause à décliner
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = Field(title="Nom")
|
||||||
|
title: str = RichtextSingleline(title="Titre")
|
||||||
label: str = ""
|
label: str = ""
|
||||||
body: str = RichtextMultiline()
|
body: str = RichtextMultiline(title="Corps")
|
||||||
|
|
||||||
@validator("label", always=True)
|
@validator("label", always=True)
|
||||||
def generate_label(cls, v, values, **kwargs):
|
def generate_label(cls, v, values, **kwargs):
|
||||||
@@ -48,6 +57,9 @@ class ProvisionTemplate(CrudDocument):
|
|||||||
class Settings(CrudDocument.Settings):
|
class Settings(CrudDocument.Settings):
|
||||||
fulltext_search = ['name', 'title', 'body']
|
fulltext_search = ['name', 'title', 'body']
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Template de clause'
|
||||||
|
|
||||||
|
|
||||||
class ProvisionTemplateReference(BaseModel):
|
class ProvisionTemplateReference(BaseModel):
|
||||||
provision_template_id: str = Field(
|
provision_template_id: str = Field(
|
||||||
@@ -58,22 +70,31 @@ class ProvisionTemplateReference(BaseModel):
|
|||||||
"displayedFields": ['title', 'body']
|
"displayedFields": ['title', 'body']
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props={"parametrized": True}
|
props={"parametrized": True},
|
||||||
|
title="Template de clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Clause'
|
||||||
|
|
||||||
|
|
||||||
class ContractTemplate(CrudDocument):
|
class ContractTemplate(CrudDocument):
|
||||||
name: str
|
"""
|
||||||
title: str
|
Modèle de contrat à décliner
|
||||||
|
"""
|
||||||
|
name: str = Field(title="Nom")
|
||||||
|
title: str = Field(title="Titre")
|
||||||
label: str = ""
|
label: str = ""
|
||||||
parties: List[PartyTemplate] = []
|
parties: List[PartyTemplate] = Field(default=[], title="Parties")
|
||||||
provisions: List[ProvisionTemplateReference] = Field(
|
provisions: List[ProvisionTemplateReference] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
props={"items-per-row": "1", "numbered": True}
|
props={"items-per-row": "1", "numbered": True},
|
||||||
|
title="Clauses"
|
||||||
)
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
format="dictionary",
|
||||||
|
title="Variables"
|
||||||
)
|
)
|
||||||
|
|
||||||
@validator("label", always=True)
|
@validator("label", always=True)
|
||||||
@@ -82,3 +103,6 @@ class ContractTemplate(CrudDocument):
|
|||||||
|
|
||||||
class Settings(CrudDocument.Settings):
|
class Settings(CrudDocument.Settings):
|
||||||
fulltext_search = ['name', 'title']
|
fulltext_search = ['name', 'title']
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Template de contrat'
|
||||||
|
|||||||
@@ -11,15 +11,23 @@ class ContractTemplateRead(ContractTemplate):
|
|||||||
|
|
||||||
|
|
||||||
class ContractTemplateCreate(Writer):
|
class ContractTemplateCreate(Writer):
|
||||||
name: str
|
name: str = Field(title="Nom")
|
||||||
title: str
|
title: str = Field(title="Titre")
|
||||||
parties: List[PartyTemplate] = []
|
parties: List[PartyTemplate] = Field(default=[], title="Parties")
|
||||||
|
provisions: List[ProvisionTemplateReference] = Field(
|
||||||
|
default=[],
|
||||||
|
props={"items-per-row": "1", "numbered": True},
|
||||||
|
title="Clauses"
|
||||||
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
format="dictionary",
|
||||||
props={"required": False}
|
props={"required": False},
|
||||||
|
title="Variables"
|
||||||
)
|
)
|
||||||
provisions: List[ProvisionTemplateReference] = []
|
|
||||||
|
class Config:
|
||||||
|
title = 'Template de Contrat'
|
||||||
|
|
||||||
|
|
||||||
class ContractTemplateUpdate(ContractTemplateCreate):
|
class ContractTemplateUpdate(ContractTemplateCreate):
|
||||||
@@ -31,9 +39,12 @@ class ProvisionTemplateRead(ProvisionTemplate):
|
|||||||
|
|
||||||
|
|
||||||
class ProvisionTemplateCreate(Writer):
|
class ProvisionTemplateCreate(Writer):
|
||||||
name: str
|
name: str = Field(title="Nom")
|
||||||
title: str = RichtextSingleline()
|
title: str = RichtextSingleline(title="Titre")
|
||||||
body: str = RichtextMultiline()
|
body: str = RichtextMultiline(title="Corps")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
title = 'Template de Clause'
|
||||||
|
|
||||||
|
|
||||||
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
|
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from .routes import router as user_router, get_auth_router
|
from .routes import router as user_router
|
||||||
|
from .manager import get_auth_router
|
||||||
from .models import User, AccessToken
|
from .models import User, AccessToken
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Dict, Generic, Optional
|
from typing import Any
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
@@ -88,10 +88,10 @@ async def get_user_manager(user_db=Depends(get_user_db)):
|
|||||||
def get_database_strategy(
|
def get_database_strategy(
|
||||||
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
|
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
|
||||||
) -> DatabaseStrategy:
|
) -> DatabaseStrategy:
|
||||||
return DatabaseStrategy(access_token_db, lifetime_seconds=3600)
|
return DatabaseStrategy(access_token_db, lifetime_seconds=None)
|
||||||
|
|
||||||
|
|
||||||
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
|
bearer_transport = BearerTransport(tokenUrl="auth/login")
|
||||||
|
|
||||||
|
|
||||||
auth_backend = AuthenticationBackend(
|
auth_backend = AuthenticationBackend(
|
||||||
@@ -107,6 +107,10 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](
|
|||||||
)
|
)
|
||||||
|
|
||||||
get_current_user = fastapi_users.current_user(active=True)
|
get_current_user = fastapi_users.current_user(active=True)
|
||||||
|
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
|
||||||
|
|
||||||
|
def get_current_user_and_firm(user=Depends(get_current_user)):
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_auth_router():
|
def get_auth_router():
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Optional, TypeVar
|
from typing import Optional, TypeVar
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import Field
|
||||||
from beanie import PydanticObjectId
|
from beanie import Document
|
||||||
|
|
||||||
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
|
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
|
||||||
from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken
|
from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, BeanieBaseAccessToken
|
||||||
@@ -9,12 +9,13 @@ from fastapi_users_db_beanie.access_token import BeanieAccessTokenDatabase, Bean
|
|||||||
from pymongo import IndexModel
|
from pymongo import IndexModel
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BeanieBaseAccessToken[PydanticObjectId]):
|
class AccessToken(BeanieBaseAccessToken, Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class User(BeanieBaseUser[PydanticObjectId]):
|
class User(BeanieBaseUser, Document):
|
||||||
login: str
|
login: str
|
||||||
|
entity_id: str
|
||||||
created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
|
created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
|
||||||
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ from typing import List
|
|||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .schemas import UserRead, UserUpdate, UserCreate
|
from .schemas import UserRead, UserUpdate, UserCreate
|
||||||
from .manager import get_user_manager, get_current_user, get_auth_router
|
from .manager import get_user_manager, get_current_user, get_current_superuser
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_description="User added to the database")
|
@router.post("/", response_description="User added to the database")
|
||||||
async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> dict:
|
async def create(user_form: UserCreate, user_manager=Depends(get_user_manager), user=Depends(get_current_superuser)) -> dict:
|
||||||
await user_manager.create(user, safe=True)
|
await user_manager.create(user_form, safe=True)
|
||||||
return {"message": "User added successfully"}
|
return {"message": "User added successfully"}
|
||||||
|
|
||||||
|
|
||||||
@@ -26,22 +26,22 @@ async def read_me(user=Depends(get_current_user)) -> UserRead:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/{id}", response_description="User record retrieved")
|
@router.get("/{id}", response_description="User record retrieved")
|
||||||
async def read_id(id: PydanticObjectId) -> UserRead:
|
async def read_id(id: PydanticObjectId, user=Depends(get_current_superuser)) -> UserRead:
|
||||||
user = await User.get(id)
|
user = await User.get(id)
|
||||||
return UserRead(**user.dict())
|
return UserRead(**user.dict())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[UserRead], response_description="User records retrieved")
|
@router.get("/", response_model=List[UserRead], response_description="User records retrieved")
|
||||||
async def read_list() -> List[UserRead]:
|
async def read_list(user=Depends(get_current_superuser)) -> List[UserRead]:
|
||||||
users = await User.find_all().to_list()
|
users = await User.find_all().to_list()
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{id}", response_description="User record updated")
|
@router.put("/{id}", response_description="User record updated")
|
||||||
async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead:
|
async def update(id: PydanticObjectId, user_form: UserUpdate, user=Depends(get_current_superuser)) -> UserRead:
|
||||||
req = {k: v for k, v in req.dict().items() if v is not None}
|
user_form = {k: v for k, v in user_form.dict().items() if v is not None}
|
||||||
update_query = {"$set": {
|
update_query = {"$set": {
|
||||||
field: value for field, value in req.items()
|
field: value for field, value in user_form.items()
|
||||||
}}
|
}}
|
||||||
|
|
||||||
user = await User.get(id)
|
user = await User.get(id)
|
||||||
@@ -56,7 +56,7 @@ async def update(id: PydanticObjectId, req: UserUpdate) -> UserRead:
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}", response_description="User record deleted from the database")
|
@router.delete("/{id}", response_description="User record deleted from the database")
|
||||||
async def delete(id: PydanticObjectId) -> dict:
|
async def delete(id: PydanticObjectId, user=Depends(get_current_superuser)) -> dict:
|
||||||
record = await User.get(id)
|
record = await User.get(id)
|
||||||
|
|
||||||
if not record:
|
if not record:
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import uuid
|
from typing import Annotated
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from fastapi_users import schemas
|
from fastapi_users import schemas
|
||||||
|
|
||||||
from ..core.schemas import Reader
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
@@ -13,17 +11,14 @@ class UserBase(schemas.CreateUpdateDictModel):
|
|||||||
|
|
||||||
|
|
||||||
class UserRead(User):
|
class UserRead(User):
|
||||||
class Config:
|
_id: Annotated[str, Field(alias='id')]
|
||||||
fields = {
|
hashed_password: Annotated[str, Field(exclude=True)]
|
||||||
'_id': {'alias': 'id'},
|
|
||||||
'hashed_password': {'exclude': True}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
login: str
|
login: str
|
||||||
password: str
|
password: str
|
||||||
email: str
|
email: str
|
||||||
|
entity_id: str
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(UserBase):
|
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
|
import json
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from app.db import init_db, Entity, Order, Contract, User, AccessToken
|
from app.db import init_db, Entity, Contract, ContractTemplate, ProvisionTemplate, User
|
||||||
|
|
||||||
|
|
||||||
models = [Entity, Order, Contract, User]
|
models = [Entity, Contract, User, ContractTemplate, ProvisionTemplate]
|
||||||
|
|
||||||
|
|
||||||
async def handle_migration(args):
|
async def handle_migration(args):
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
fastapi==0.88.0
|
fastapi
|
||||||
fastapi_users==10.2.1
|
fastapi_users
|
||||||
fastapi_users_db_beanie==1.1.2
|
fastapi_users_db_beanie
|
||||||
motor==3.1.1
|
fastapi-pagination
|
||||||
fastapi-paginate==0.1.0
|
|
||||||
uvicorn
|
uvicorn
|
||||||
jinja2
|
jinja2
|
||||||
weasyprint
|
weasyprint
|
||||||
|
|||||||
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:
|
||||||
@@ -1,39 +1,58 @@
|
|||||||
version: "3.9"
|
|
||||||
services:
|
services:
|
||||||
back:
|
back:
|
||||||
build:
|
build:
|
||||||
context: ./back
|
context: ./back
|
||||||
|
image: cht-lawfirm-back-dev
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./back/app:/code/app
|
- ./back/app:/code/app
|
||||||
|
- ./back/media:/code/media
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.back.entrypoints=web"
|
||||||
|
- "traefik.http.routers.back.rule=PathPrefix(`/api/v1/`)"
|
||||||
|
- "traefik.http.services.back.loadbalancer.server.port=8000"
|
||||||
|
|
||||||
front:
|
front:
|
||||||
build:
|
build:
|
||||||
context: ./front
|
context: ./front
|
||||||
|
image: cht-lawfirm-front-dev
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "4200:4200"
|
- "4200:4200"
|
||||||
volumes:
|
volumes:
|
||||||
- ./front/app/src:/app/src
|
- ./front/app/src:/app/src
|
||||||
- ./front/app/public:/app/public
|
- ./front/app/public:/app/public
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.front.entrypoints=web"
|
||||||
|
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
||||||
|
- "traefik.http.services.front.loadbalancer.server.port=4200"
|
||||||
|
|
||||||
nginx:
|
proxy:
|
||||||
build:
|
image: traefik
|
||||||
context: ./nginx
|
|
||||||
restart: always
|
restart: always
|
||||||
|
command:
|
||||||
|
- --providers.docker
|
||||||
|
- --providers.docker.watch=true
|
||||||
|
- --providers.docker.exposedByDefault=false
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --log.level=DEBUG
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
image: "mongo"
|
image: "mongo:4.4.19"
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "27017:27017"
|
- "27017:27017"
|
||||||
environment:
|
environment:
|
||||||
MONGO_INITDB_ROOT_USERNAME: root
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
MONGO_INITDB_ROOT_PASSWORD: example
|
MONGO_INITDB_ROOT_PASSWORD: IBO3eber0mdw2R9pnInLdtFykQFY2f06
|
||||||
volumes:
|
volumes:
|
||||||
- database:/data/db
|
- database:/data/db
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
FROM node:lts-alpine
|
FROM node:lts-alpine
|
||||||
|
|
||||||
# install simple http server for serving static content
|
|
||||||
RUN npm install -g http-server
|
|
||||||
|
|
||||||
# make the 'app' folder the current working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN npm install -g @angular/cli http-server
|
||||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
|
||||||
COPY app/package*.json ./
|
COPY app/package*.json ./
|
||||||
|
|
||||||
# install project dependencies
|
|
||||||
RUN npm install -g @angular/cli
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# copy project files and folders to the current working directory (i.e. 'app' folder)
|
|
||||||
COPY app/ .
|
COPY app/ .
|
||||||
|
|
||||||
# build app for production with minification
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
EXPOSE 4200
|
EXPOSE 4200
|
||||||
|
|||||||
17
front/Dockerfile.prod
Normal file
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": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "en-US",
|
||||||
|
"locales": {
|
||||||
|
"fr": {
|
||||||
|
"translation": "src/locale/messages.fr.xlf",
|
||||||
|
"baseHref": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
|
"localize": ["fr"],
|
||||||
|
"i18nMissingTranslation": "warning",
|
||||||
"outputPath": "dist/app",
|
"outputPath": "dist/app",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
@@ -65,14 +76,22 @@
|
|||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "app:build:development"
|
"browserTarget": "app:build:development"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"browserTarget": "app:build:fr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "app:build"
|
"browserTarget": "app:build",
|
||||||
|
"format": "xlf2",
|
||||||
|
"outputPath": "src/locale",
|
||||||
|
"targetFiles": [
|
||||||
|
"messages.fr.xlf"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
|||||||
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,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^15.0.0",
|
"@angular/animations": "^15.0.0",
|
||||||
|
"@angular/cdk": "^15.2.1",
|
||||||
"@angular/common": "^15.0.0",
|
"@angular/common": "^15.0.0",
|
||||||
"@angular/compiler": "^15.0.0",
|
"@angular/compiler": "^15.0.0",
|
||||||
"@angular/core": "^15.0.0",
|
"@angular/core": "^15.0.0",
|
||||||
@@ -23,6 +24,8 @@
|
|||||||
"@ngx-formly/core": "^6.0.0",
|
"@ngx-formly/core": "^6.0.0",
|
||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"@tinymce/tinymce-angular": "^7.0.0",
|
"@tinymce/tinymce-angular": "^7.0.0",
|
||||||
|
"@types/fabric": "^5.3.0",
|
||||||
|
"fabric": "^5.3.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.1",
|
"ngx-bootstrap-icons": "^1.9.1",
|
||||||
"ngx-wig": "^15.1.4",
|
"ngx-wig": "^15.1.4",
|
||||||
"rxjs": "~7.5.0",
|
"rxjs": "~7.5.0",
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.0.0",
|
"karma-jasmine-html-reporter": "~2.0.0",
|
||||||
|
"ng-extract-i18n-merge": "^2.5.1",
|
||||||
"typescript": "~4.8.2"
|
"typescript": "~4.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import {ContractsModule} from "./views/contracts/contracts.module";
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -32,6 +31,11 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'contract-drafts',
|
path: 'contract-drafts',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./views/contract-drafts/contract-drafts.module').then((m) => m.ContractDraftsModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'contracts',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./views/contracts/contracts.module').then((m) => m.ContractsModule)
|
import('./views/contracts/contracts.module').then((m) => m.ContractsModule)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;">
|
<main style="margin-top: 0px;">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row flex-nowrap">
|
<div class="row flex-nowrap">
|
||||||
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
|
<div class="sidenav col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
|
||||||
<sidenav class="sticky-top"></sidenav>
|
<sidenav class="sticky-top"></sidenav>
|
||||||
</div>
|
</div>
|
||||||
<div class="col py-3">
|
<div class="col py-3">
|
||||||
@@ -13,3 +13,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import {Title} from "@angular/platform-browser";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -6,5 +7,9 @@ import { Component } from '@angular/core';
|
|||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'app';
|
title = 'Cooper, Hillman & Toshi';
|
||||||
|
|
||||||
|
constructor(private titleService: Title) {
|
||||||
|
titleService.setTitle(this.title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core';
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons';
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons';
|
||||||
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@@ -9,20 +11,31 @@ import { AppComponent } from './app.component';
|
|||||||
import { SidenavComponent } from "./layout/sidenav/sidenav.component";
|
import { SidenavComponent } from "./layout/sidenav/sidenav.component";
|
||||||
import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component";
|
import { FlashmessagesComponent } from "./layout/flashmessages/flashmessages.component";
|
||||||
import { FlashmessagesService } from "./layout/flashmessages/flashmessages.service";
|
import { FlashmessagesService } from "./layout/flashmessages/flashmessages.service";
|
||||||
|
import { LoginComponent, LogoutComponent } from "./layout/auth/auth.component";
|
||||||
|
import { AuthService } from "./layout/auth/auth.service";
|
||||||
|
import { AuthInterceptor } from "./layout/auth/auth.interceptor"
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
SidenavComponent,
|
SidenavComponent,
|
||||||
FlashmessagesComponent
|
FlashmessagesComponent,
|
||||||
|
LoginComponent,
|
||||||
|
LogoutComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
NgbModule,
|
NgbModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
FlashmessagesService,
|
||||||
|
AuthService,
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
|
||||||
],
|
],
|
||||||
providers: [FlashmessagesService],
|
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
|||||||
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">
|
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||||
<ngb-toast
|
<ngb-toast
|
||||||
*ngFor="let flashmessage of flashmessagesService.toasts"
|
*ngFor="let flashmessage of flashmessagesService.toasts"
|
||||||
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 1500"
|
[header]="flashmessage.type" [autohide]="true" [delay]="flashmessage.delay || 5000"
|
||||||
(hiddden)="flashmessagesService.remove(flashmessage)"
|
(hiddden)="flashmessagesService.remove(flashmessage)"
|
||||||
>
|
>
|
||||||
<ng-container [ngSwitch]="flashmessage.type">
|
<ng-container [ngSwitch]="flashmessage.type">
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.logout {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<div class="pt-2 text-white min-vh-100 text-nowrap">
|
<div class="pt-2 text-white min-vh-100 text-nowrap">
|
||||||
<a routerLink="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
<a routerLink="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||||
<span class="fs-5 d-none d-sm-inline">Cooper, Hillman &<br/>Toshi LLP</span>
|
<img class="fs-5 d-none d-sm-inline w-100" src="/assets/logo.png" alt="Cooper, Hillman & Toshi LLP">
|
||||||
</a>
|
</a>
|
||||||
<ul class="nav nav-pills flex-column align-items-sm-start mb-sm-auto mb-0 w-100" id="menu">
|
<ul class="nav nav-pills flex-column align-items-sm-start mb-sm-auto mb-0 w-100" id="menu">
|
||||||
|
<li><login></login></li>
|
||||||
|
<ng-container *ngIf="isAuthenticated">
|
||||||
<li *ngFor="let item of Menu" class="nav-item w-100">
|
<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)">
|
<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>
|
<i-bs [name]="item.icon"></i-bs><span class="ms-1 d-none d-sm-inline" [innerHTML]="item.title"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
<li class="logout"><logout></logout></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { IconNamesEnum } from "ngx-bootstrap-icons";
|
import { IconNamesEnum } from "ngx-bootstrap-icons";
|
||||||
|
import { AuthService } from "../auth/auth.service";
|
||||||
|
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
title: string,
|
||||||
|
link: string,
|
||||||
|
icon: IconNamesEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sidenav",
|
selector: "sidenav",
|
||||||
@@ -8,37 +17,52 @@ import { IconNamesEnum } from "ngx-bootstrap-icons";
|
|||||||
styleUrls: ["./sidenav.component.css"]
|
styleUrls: ["./sidenav.component.css"]
|
||||||
})
|
})
|
||||||
export class SidenavComponent {
|
export class SidenavComponent {
|
||||||
Menu = [
|
Menu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: $localize`Dashboard`,
|
||||||
link: "/dashboard",
|
link: "/dashboard",
|
||||||
icon: IconNamesEnum.HouseFill
|
icon: IconNamesEnum.HouseFill
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Entities",
|
title: $localize`Entities`,
|
||||||
link: "/entities",
|
link: "/entities",
|
||||||
icon: IconNamesEnum.PeopleFill
|
icon: IconNamesEnum.PeopleFill
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Provision Templates",
|
title: $localize`Provision Templates`,
|
||||||
link: "/templates/provisions",
|
link: "/templates/provisions",
|
||||||
icon: IconNamesEnum.BlockquoteLeft
|
icon: IconNamesEnum.BlockquoteLeft
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Contracts Templates",
|
title: $localize`Contracts Templates`,
|
||||||
link: "/templates/contracts",
|
link: "/templates/contracts",
|
||||||
icon: IconNamesEnum.FileCodeFill
|
icon: IconNamesEnum.FileCodeFill
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Contracts Drafts",
|
title: $localize`Contracts Drafts`,
|
||||||
link: "/contract-drafts",
|
link: "/contract-drafts",
|
||||||
icon: IconNamesEnum.PencilSquare
|
icon: IconNamesEnum.PencilSquare
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: $localize`Contracts`,
|
||||||
|
link: "/contracts",
|
||||||
|
icon: IconNamesEnum.FileEarmarkTextFill
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor(private router: Router) {}
|
isAuthenticated: boolean = false
|
||||||
|
|
||||||
is_current_page(menu_item: any) {
|
constructor(
|
||||||
return this.router.url.indexOf(menu_item.link) > -1;
|
private router: Router,
|
||||||
|
private authService: AuthService,) {
|
||||||
|
|
||||||
|
this.isAuthenticated = this.authService.is_authenticated;
|
||||||
|
this.authService.onIsAuthenticated$.subscribe((isAuthenticated: boolean) => {
|
||||||
|
this.isAuthenticated = isAuthenticated;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
is_current_page(menu_item: MenuItem) {
|
||||||
|
return this.router.url.startsWith(menu_item.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
[schema]="this.schema"
|
[schema]="this.schema"
|
||||||
(resourceUpdated)="this.flashService.success('Entity updated')"
|
(resourceUpdated)="this.flashService.success('Entity updated')"
|
||||||
(resourceDeleted)="this.flashService.success('Entity deleted')"
|
(resourceDeleted)="this.flashService.success('Entity deleted')"
|
||||||
|
(resourceReceived)="this.onResourceReceived($event)"
|
||||||
(error)="this.flashService.error($event)">
|
(error)="this.flashService.error($event)">
|
||||||
</crud-card>
|
</crud-card>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Component, Input} from "@angular/core";
|
import {Component, EventEmitter, Input, Output} from "@angular/core";
|
||||||
import {ActivatedRoute, ParamMap} from "@angular/router";
|
import {ActivatedRoute, ParamMap} from "@angular/router";
|
||||||
import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service";
|
import { FlashmessagesService } from "../../../layout/flashmessages/flashmessages.service";
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@ export class BaseCrudCardComponent {
|
|||||||
@Input() resource_id: string | null = null;
|
@Input() resource_id: string | null = null;
|
||||||
@Input() schema: string | undefined;
|
@Input() schema: string | undefined;
|
||||||
|
|
||||||
|
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
public flashService: FlashmessagesService
|
public flashService: FlashmessagesService
|
||||||
@@ -23,4 +25,8 @@ export class BaseCrudCardComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResourceReceived(model: any) {
|
||||||
|
this.resourceReceived.emit(model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
[resource]="this.resource"
|
[resource]="this.resource"
|
||||||
[schema]="this.schema"
|
[schema]="this.schema"
|
||||||
[columns]="this.columns"
|
[columns]="this.columns"
|
||||||
|
[filters]="this.filters"
|
||||||
(result)="this.flashService.success($event)"
|
(result)="this.flashService.success($event)"
|
||||||
(error)="this.flashService.error($event)">
|
(error)="this.flashService.error($event)">
|
||||||
</crud-list>
|
</crud-list>
|
||||||
@@ -9,6 +9,7 @@ export class BaseCrudListComponent {
|
|||||||
@Input() resource: string = "";
|
@Input() resource: string = "";
|
||||||
@Input() columns: string[] = [];
|
@Input() columns: string[] = [];
|
||||||
@Input() schema: string | undefined;
|
@Input() schema: string | undefined;
|
||||||
|
@Input() filters: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public flashService: FlashmessagesService
|
public flashService: FlashmessagesService
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<base-card
|
<base-card
|
||||||
[resource]="this.resource"
|
[resource]="this.resource"
|
||||||
[schema]="this.schema">
|
[schema]="this.schema">
|
||||||
</base-card>'
|
</base-card>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<base-list
|
<base-list
|
||||||
[resource]="this.resource"
|
[resource]="this.resource"
|
||||||
[schema]="this.schema"
|
[schema]="this.schema"
|
||||||
[columns]="this.columns">
|
[columns]="this.columns"
|
||||||
|
[filters]="this.filters">
|
||||||
</base-list>
|
</base-list>
|
||||||
@@ -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,40 +1,48 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { DraftCardComponent, DraftListComponent, DraftNewComponent } from "./drafts.component";
|
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent} from "./contracts.component";
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
data: {
|
data: {
|
||||||
title: 'Entities',
|
title: 'Contracts',
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{ path: '', redirectTo: 'list', pathMatch: 'full' },
|
{ path: '', redirectTo: 'list', pathMatch: 'full' },
|
||||||
|
{ path: 'drafts', redirectTo: '/contract-drafts/list' },
|
||||||
{
|
{
|
||||||
path: 'list',
|
path: 'list',
|
||||||
component: DraftListComponent,
|
component: ContractsListComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'List',
|
title: 'List',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
component: DraftNewComponent,
|
component: ContractsNewComponent,
|
||||||
|
data: {
|
||||||
|
title: 'New',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signature/:id',
|
||||||
|
component: ContractsSignatureComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'New',
|
title: 'New',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: DraftCardComponent,
|
component: ContractsCardComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'Card',
|
title: 'Card',
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
137
front/app/src/app/views/contracts/contracts.component.ts
Normal file
137
front/app/src/app/views/contracts/contracts.component.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ParamMap } from "@angular/router";
|
||||||
|
import { DomSanitizer } from "@angular/platform-browser";
|
||||||
|
import { ImageUploaderCrudService } from "@common/crud/crud.service";
|
||||||
|
|
||||||
|
|
||||||
|
export class BaseContractsComponent {
|
||||||
|
protected resource: string = "contract";
|
||||||
|
protected schema: string = "Contract";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: '../base-view/templates/list.template.html'
|
||||||
|
})
|
||||||
|
export class ContractsListComponent extends BaseContractsComponent {
|
||||||
|
columns = ['status', 'name', 'title', 'parties.items.entity.label', 'lawyer.label', 'date'];
|
||||||
|
filters = ['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: '../base-view/templates/new.template.html'
|
||||||
|
})
|
||||||
|
export class ContractsNewComponent extends BaseContractsComponent {
|
||||||
|
@Input() value: {} = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template:`
|
||||||
|
<ng-container *ngIf="this.resourceReadyToPrint; else previewLink">
|
||||||
|
<label i18n>Download Link:</label>
|
||||||
|
<div class="input-group mb-12">
|
||||||
|
<span class="input-group-text"><a href="{{ this.contractPrintLink! }}" target="_blank">{{ this.contractPrintLink! }}</a></span>
|
||||||
|
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPrintLink!"><i-bs name="text-paragraph"/></button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #previewLink>
|
||||||
|
<label i18n>Preview Link:</label>
|
||||||
|
<div class="input-group mb-12">
|
||||||
|
<span class="input-group-text"><a href="{{ this.contractPreviewLink! }}" target="_blank">{{ this.contractPreviewLink! }}</a></span>
|
||||||
|
<button type="button" class="btn btn-light" [cdkCopyToClipboard]="this.contractPreviewLink!"><i-bs name="text-paragraph"/></button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<base-card
|
||||||
|
[resource_id]="this.resource_id"
|
||||||
|
[resource]="this.resource"
|
||||||
|
[schema]="this.schema"
|
||||||
|
(resourceReceived)="this.onResourceReceived($event)">
|
||||||
|
</base-card>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ContractsCardComponent extends BaseContractsComponent{
|
||||||
|
resource_id: string | null = null;
|
||||||
|
resourceReadyToPrint = false;
|
||||||
|
contractPrintLink: string | null = null;
|
||||||
|
contractPreviewLink: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.resource_id === null) {
|
||||||
|
this.route.paramMap.subscribe((params: ParamMap) => {
|
||||||
|
this.resource_id = params.get('id')
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResourceReceived(model: any): void {
|
||||||
|
this.resourceReadyToPrint = model.status != "published";
|
||||||
|
this.contractPrintLink = `${location.origin}/api/v1/contract/print/pdf/${this.resource_id}`
|
||||||
|
this.contractPreviewLink = `${location.origin}/api/v1/contract/print/preview/${this.resource_id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="row" *ngIf="!this.affixed">
|
||||||
|
<signature-drawer class="col-7"
|
||||||
|
(signatureDrawn$)="postSignature($event)"></signature-drawer>
|
||||||
|
<div class="col-5" i18n>
|
||||||
|
<p>Cette page est à la destination exclusive de <strong>{{ this.signatory }}</strong></p>
|
||||||
|
<p>Si vous n'êtes <strong>pas</strong> {{ this.signatory }}, veuillez <strong>fermer cette page immédiatement</strong> et surpprimer tous les liens en votre possession menant vers celle-ci.</p>
|
||||||
|
<p>En vous maintenant et/ou en interagissant avec cette page, vous enfreignez l'article L.229 du code pénal de l'Etat de San Andreas pour <strong>usurpation d'identité</strong> et vous vous exposez ainsi à une amende de 20 000$ ainsi qu'à des poursuites civiles.</p>
|
||||||
|
<p>Le cabinet Cooper, Hillman & Toshi LLC</p>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
export class ContractsSignatureComponent implements OnInit {
|
||||||
|
signature_id: string | null = null;
|
||||||
|
signature: any = {}
|
||||||
|
signatory = ""
|
||||||
|
|
||||||
|
affixed = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private crudService: ImageUploaderCrudService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.paramMap.subscribe((params: ParamMap) => {
|
||||||
|
this.signature_id = params.get('id');
|
||||||
|
this.crudService.get('contract/signature', this.signature_id!).subscribe( (response:any) => {
|
||||||
|
this.signature = response;
|
||||||
|
this.affixed = this.signature.signature_affixed;
|
||||||
|
if (this.signature.representative) {
|
||||||
|
this.signatory = this.signature.representative.entity_data.firstname + " " + this.signature.representative.entity_data.lastname;
|
||||||
|
} else {
|
||||||
|
this.signatory = this.signature.entity.entity_data.firstname + " " + this.signature.entity.entity_data.lastname;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreview() {
|
||||||
|
return this.sanitizer.bypassSecurityTrustResourceUrl("/api/v1/contract/print/preview/signature/" + this.signature_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
postSignature(image: string) {
|
||||||
|
this.crudService.upload('contract/signature', this.signature_id!, image).subscribe((v: any) => {
|
||||||
|
if (v) {
|
||||||
|
this.affixed = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,16 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { BaseViewModule } from "../base-view/base-view.module";
|
import { BaseViewModule } from "../base-view/base-view.module";
|
||||||
import { ContractsRoutingModule } from './contracts-routing.module';
|
import { ContractsRoutingModule } from './contracts-routing.module';
|
||||||
import { DraftCardComponent, DraftListComponent, DraftNewComponent, DraftNewFormComponent } from "./drafts.component";
|
|
||||||
import { FormlyModule } from "@ngx-formly/core";
|
import { FormlyModule } from "@ngx-formly/core";
|
||||||
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
|
import { FormlyBootstrapModule } from "@ngx-formly/bootstrap";
|
||||||
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
|
import { ForeignkeyTypeComponent } from "@common/crud/types/foreignkey.type";
|
||||||
import { CrudService } from "@common/crud/crud.service";
|
import { CrudService, ImageUploaderCrudService } from "@common/crud/crud.service";
|
||||||
|
|
||||||
|
import { NgbAccordionModule, NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
|
||||||
|
import { ContractsCardComponent, ContractsListComponent, ContractsNewComponent, ContractsSignatureComponent } from "../contracts/contracts.component";
|
||||||
|
import { AlphaRangeComponent, BlackBlueRangeComponent, SignatureDrawerComponent } from "./signature-drawer/signature-drawer.component";
|
||||||
|
import { ClipboardModule } from "@angular/cdk/clipboard";
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -15,20 +20,27 @@ import { CrudService } from "@common/crud/crud.service";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
BaseViewModule,
|
BaseViewModule,
|
||||||
ContractsRoutingModule,
|
ContractsRoutingModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
NgbCollapseModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
FormlyModule.forRoot({
|
FormlyModule.forRoot({
|
||||||
types: [
|
types: [
|
||||||
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
|
{ name: 'foreign-key', component: ForeignkeyTypeComponent }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
FormlyBootstrapModule,
|
FormlyBootstrapModule,
|
||||||
|
ClipboardModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
DraftListComponent,
|
ContractsListComponent,
|
||||||
DraftNewComponent,
|
ContractsNewComponent,
|
||||||
DraftCardComponent,
|
ContractsCardComponent,
|
||||||
DraftNewFormComponent
|
ContractsSignatureComponent,
|
||||||
|
SignatureDrawerComponent,
|
||||||
|
BlackBlueRangeComponent,
|
||||||
|
AlphaRangeComponent
|
||||||
],
|
],
|
||||||
providers: [CrudService]
|
providers: [CrudService, ImageUploaderCrudService]
|
||||||
})
|
})
|
||||||
export class ContractsModule {
|
export class ContractsModule {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
export class EntityListComponent extends BaseEntitiesComponent {
|
||||||
columns = ['label', 'address', 'entity_data.type']
|
columns = ['label', 'address', 'entity_data.type']
|
||||||
|
filters = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { EntitiesRoutingModule } from './entities-routing.module';
|
import { EntitiesRoutingModule } from './entities-routing.module';
|
||||||
import { EntityCardComponent, EntityListComponent, EntityNewComponent} from "./entities.component";
|
|
||||||
import { BaseViewModule } from "../base-view/base-view.module";
|
import { BaseViewModule } from "../base-view/base-view.module";
|
||||||
|
import { EntityCardComponent, EntityListComponent, EntityNewComponent} from "./entities.component";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export class BaseContractTemplateComponent {
|
|||||||
templateUrl: '../base-view/templates/list.template.html'
|
templateUrl: '../base-view/templates/list.template.html'
|
||||||
})
|
})
|
||||||
export class ContractTemplateListComponent extends BaseContractTemplateComponent {
|
export class ContractTemplateListComponent extends BaseContractTemplateComponent {
|
||||||
columns = [];
|
columns = ['name', 'title', 'parties.items.part'];
|
||||||
|
filters = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export class BaseProvisionTemplateComponent {
|
|||||||
templateUrl: '../base-view/templates/list.template.html'
|
templateUrl: '../base-view/templates/list.template.html'
|
||||||
})
|
})
|
||||||
export class ProvisionTemplateListComponent extends BaseProvisionTemplateComponent{
|
export class ProvisionTemplateListComponent extends BaseProvisionTemplateComponent{
|
||||||
columns = [];
|
columns = ['name', 'title', 'body'];
|
||||||
|
filters = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
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>
|
<div>
|
||||||
<form cForm [formGroup]="form" (ngSubmit)="onSubmit(model)">
|
<form cForm [formGroup]="form" (ngSubmit)="onSubmit(model)">
|
||||||
<span class="col col-form-label" *ngIf="formLoading$ || modelLoading$ | async">Loading...</span>
|
<span class="col col-form-label" i18n *ngIf="formLoading$ || modelLoading$ | async">Loading...</span>
|
||||||
<formly-form [form]="form" [fields]="fields" [model]="model"></formly-form>
|
<formly-form [form]="form" [fields]="fields" [model]="model"></formly-form>
|
||||||
<div class="d-grid gap-2 d-md-flex">
|
<div class="d-grid gap-2 d-md-flex">
|
||||||
<button class="btn btn-success btn-lg" type="submit"
|
<button class="btn btn-success btn-lg" type="submit"
|
||||||
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)">
|
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)">
|
||||||
{{ this.isCreateForm() ? "Create" : "Update" }}
|
{{ submitText }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary btn-lg" type="button" *ngIf="!this.isCreateForm()"
|
<button class="btn btn-primary btn-lg" i18n type="button" *ngIf="!this.isCreateForm()"
|
||||||
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)"
|
[disabled]="!form.valid && (formLoading$ || modelLoading$ | async)"
|
||||||
(click)="open(duplicationModal)">
|
(click)="open(duplicationModal)">Duplicate</button>
|
||||||
Duplicate
|
<button class="btn btn-danger btn-lg" i18n type="button" *ngIf="!this.isCreateForm()"
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger btn-lg" type="button" *ngIf="!this.isCreateForm()"
|
|
||||||
[disabled]="formLoading$ || modelLoading$ | async"
|
[disabled]="formLoading$ || modelLoading$ | async"
|
||||||
(click)="open(confirmDeleteModal)">
|
(click)="open(confirmDeleteModal)">Delete</button>
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
<ng-template #confirmDeleteModal let-modal>
|
<ng-template #confirmDeleteModal let-modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Are you sure you want to delete this {{ this.schema }}?</h4>
|
<h4 class="modal-title" i18n>Are you sure you want to delete this {{ this.schema }}?</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
|
<button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-light" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')">Cancel</button>
|
<button type="button" class="btn btn-light" i18n i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')">Cancel</button>
|
||||||
<button type="button" class="btn btn-danger" (click)="modal.close('Save click')">Delete</button>
|
<button type="button" class="btn btn-danger" i18n (click)="modal.close('Save click')">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #duplicationModal let-modal>
|
<ng-template #duplicationModal let-modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">Duplicate {{ this.schema }}</h4>
|
<h4 class="modal-title" i18n>Duplicate {{ this.schema }}</h4>
|
||||||
<button type="button" class="btn-close" aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
|
<button type="button" class="btn-close" i18n-aria-label aria-label="Cancel" (click)="modal.dismiss('Cancel Deletion')"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<crud-card [resource]="this.resource"
|
<crud-card [resource]="this.resource"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class CardComponent implements OnInit {
|
|||||||
@Output() resourceUpdated: EventEmitter<string> = new EventEmitter();
|
@Output() resourceUpdated: EventEmitter<string> = new EventEmitter();
|
||||||
@Output() resourceDeleted: EventEmitter<string> = new EventEmitter();
|
@Output() resourceDeleted: EventEmitter<string> = new EventEmitter();
|
||||||
@Output() error: EventEmitter<string> = new EventEmitter();
|
@Output() error: EventEmitter<string> = new EventEmitter();
|
||||||
|
@Output() resourceReceived: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
form = new FormGroup({});
|
form = new FormGroup({});
|
||||||
fields: FormlyFieldConfig[] = [];
|
fields: FormlyFieldConfig[] = [];
|
||||||
@@ -55,6 +55,10 @@ export class CardComponent implements OnInit {
|
|||||||
return this._modelLoading$.asObservable();
|
return this._modelLoading$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get submitText() {
|
||||||
|
return this.isCreateForm() ? $localize`Create` : $localize`Update`
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private crudService: CrudService,
|
constructor(private crudService: CrudService,
|
||||||
private formlyJsonschema: CrudFormlyJsonschemaService,
|
private formlyJsonschema: CrudFormlyJsonschemaService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -74,6 +78,7 @@ export class CardComponent implements OnInit {
|
|||||||
next :(model: any) => {
|
next :(model: any) => {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this._modelLoading$.next(false);
|
this._modelLoading$.next(false);
|
||||||
|
this.resourceReceived.emit(model);
|
||||||
},
|
},
|
||||||
error: (err) => this.error.emit("Error loading the model:" + err)
|
error: (err) => this.error.emit("Error loading the model:" + err)
|
||||||
});
|
});
|
||||||
@@ -107,9 +112,10 @@ export class CardComponent implements OnInit {
|
|||||||
model._id = this.resource_id;
|
model._id = this.resource_id;
|
||||||
this.crudService.update(this.resource!, model).subscribe( {
|
this.crudService.update(this.resource!, model).subscribe( {
|
||||||
next: (model: any) => {
|
next: (model: any) => {
|
||||||
|
this.resourceUpdated.emit(model._id);
|
||||||
|
this.resourceReceived.emit(model);
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this._modelLoading$.next(false);
|
this._modelLoading$.next(false);
|
||||||
this.resourceUpdated.emit(model._id)
|
|
||||||
},
|
},
|
||||||
error: (err) => this.error.emit("Error updating the entity:" + err)
|
error: (err) => this.error.emit("Error updating the entity:" + err)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,13 +43,20 @@ export class CrudFormlyJsonschemaOptions implements FormlyJsonschemaOptions {
|
|||||||
field.type = "datetime";
|
field.type = "datetime";
|
||||||
} else if (schema.format === 'date') {
|
} else if (schema.format === 'date') {
|
||||||
field.type = "date";
|
field.type = "date";
|
||||||
} else if (schema.hasOwnProperty('enum') && schema.enum.length == 1 && schema.enum[0] == schema.default ) {
|
} else if (
|
||||||
|
(schema.hasOwnProperty('hidden') && schema.hidden)
|
||||||
|
|| (schema.hasOwnProperty('enum') && schema.enum.length == 1 && schema.enum[0] == schema.default)
|
||||||
|
) {
|
||||||
field.type = "hidden";
|
field.type = "hidden";
|
||||||
} else if (schema.type == "array" && schema.format == "dictionary") {
|
} else if (schema.type == "array" && schema.format == "dictionary") {
|
||||||
field.type = "dictionary";
|
field.type = "dictionary";
|
||||||
} else if (schema.type == "string" && schema.hasOwnProperty('props')
|
} else if (schema.type == "string" && schema.hasOwnProperty('props')
|
||||||
&& schema.props.hasOwnProperty("richtext") && schema.props.richtext) {
|
&& schema.props.hasOwnProperty("richtext") && schema.props.richtext) {
|
||||||
field.type = "richtext";
|
field.type = "richtext";
|
||||||
|
} else if (schema.type == "string" && schema.format == "signature-link") {
|
||||||
|
field.type = "signature-link";
|
||||||
|
} else if (field.type == "enum" && field.props.multiple) {
|
||||||
|
field.type = 'multicheckbox';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.hasOwnProperty('props')) {
|
if (schema.hasOwnProperty('props')) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ListComponent } from "./list/list.component";
|
|||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: ListComponent },
|
{ path: '', component: ListComponent },
|
||||||
{ path: ':id', component: CardComponent },
|
{ path: ':id', component: CardComponent },
|
||||||
];;
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ArrayTypeComponent } from "./types/array.type";
|
|||||||
import { ObjectTypeComponent } from "./types/object.type";
|
import { ObjectTypeComponent } from "./types/object.type";
|
||||||
import { DatetimeTypeComponent } from "./types/datetime.type";
|
import { DatetimeTypeComponent } from "./types/datetime.type";
|
||||||
import { DateTypeComponent } from "./types/date.type";
|
import { DateTypeComponent } from "./types/date.type";
|
||||||
import { ApiService, CrudService } from "./crud.service";
|
import { ApiService, CrudService, ImageUploaderCrudService } from "./crud.service";
|
||||||
import { CrudFormlyJsonschemaService } from "./crud-formly-jsonschema.service";
|
import { CrudFormlyJsonschemaService } from "./crud-formly-jsonschema.service";
|
||||||
import { NgbModule} from "@ng-bootstrap/ng-bootstrap";
|
import { NgbModule} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
|
import { allIcons, NgxBootstrapIconsModule } from "ngx-bootstrap-icons";
|
||||||
@@ -26,12 +26,16 @@ import { HiddenTypeComponent } from "./types/hidden.type";
|
|||||||
import { DictionaryTypeComponent } from "./types/dictionary.type";
|
import { DictionaryTypeComponent } from "./types/dictionary.type";
|
||||||
import { DictionaryService } from "./types/dictionary.service";
|
import { DictionaryService } from "./types/dictionary.service";
|
||||||
import { RichtextTypeComponent } from "./types/richtext.type";
|
import { RichtextTypeComponent } from "./types/richtext.type";
|
||||||
|
import { SignatureLinkTypeComponent } from "@common/crud/types/signature-link.type";
|
||||||
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
|
import {FilterListComponent} from "@common/crud/list/filter-list.component";
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CardComponent,
|
CardComponent,
|
||||||
ListComponent,
|
ListComponent,
|
||||||
|
FilterListComponent,
|
||||||
ObjectTypeComponent,
|
ObjectTypeComponent,
|
||||||
DatetimeTypeComponent,
|
DatetimeTypeComponent,
|
||||||
DateTypeComponent,
|
DateTypeComponent,
|
||||||
@@ -40,12 +44,14 @@ import { RichtextTypeComponent } from "./types/richtext.type";
|
|||||||
ForeignkeyTypeComponent,
|
ForeignkeyTypeComponent,
|
||||||
HiddenTypeComponent,
|
HiddenTypeComponent,
|
||||||
DictionaryTypeComponent,
|
DictionaryTypeComponent,
|
||||||
RichtextTypeComponent
|
RichtextTypeComponent,
|
||||||
|
SignatureLinkTypeComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
JsonschemasService,
|
JsonschemasService,
|
||||||
ApiService,
|
ApiService,
|
||||||
CrudService,
|
CrudService,
|
||||||
|
ImageUploaderCrudService,
|
||||||
CrudFormlyJsonschemaService,
|
CrudFormlyJsonschemaService,
|
||||||
DictionaryService
|
DictionaryService
|
||||||
],
|
],
|
||||||
@@ -68,10 +74,12 @@ import { RichtextTypeComponent } from "./types/richtext.type";
|
|||||||
{ name: 'hidden', component: HiddenTypeComponent },
|
{ name: 'hidden', component: HiddenTypeComponent },
|
||||||
{ name: 'dictionary', component: DictionaryTypeComponent },
|
{ name: 'dictionary', component: DictionaryTypeComponent },
|
||||||
{ name: 'richtext', component: RichtextTypeComponent },
|
{ name: 'richtext', component: RichtextTypeComponent },
|
||||||
|
{ name: 'signature-link', component: SignatureLinkTypeComponent },
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
FormlyBootstrapModule,
|
FormlyBootstrapModule,
|
||||||
EditorModule
|
EditorModule,
|
||||||
|
ClipboardModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CardComponent,
|
CardComponent,
|
||||||
|
|||||||
@@ -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) {
|
buildResource(resourceName: string) {
|
||||||
let resource;
|
let resource;
|
||||||
|
|
||||||
resource = { ... this.rawSchemas.components.schemas[resourceName]};
|
resource = structuredClone(this.rawSchemas.components.schemas[resourceName]);
|
||||||
resource.components = { schemas: {} };
|
resource.components = { schemas: {} };
|
||||||
for (let prop_name in resource.properties) {
|
for (let prop_name in resource.properties) {
|
||||||
let prop = resource.properties[prop_name];
|
let prop = resource.properties[prop_name];
|
||||||
@@ -94,12 +94,34 @@ export class JsonschemasService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.changePropertiesOrder(resource);
|
||||||
observer.next(resource);
|
observer.next(resource);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changePropertiesOrder(resource: any) {
|
||||||
|
let created_at;
|
||||||
|
let updated_at;
|
||||||
|
let new_properties: any = {};
|
||||||
|
for (let prop_name in resource.properties) {
|
||||||
|
if (prop_name == 'created_at') {
|
||||||
|
created_at = resource.properties[prop_name];
|
||||||
|
} else if (prop_name == 'updated_at') {
|
||||||
|
updated_at = resource.properties[prop_name];
|
||||||
|
} else {
|
||||||
|
new_properties[prop_name] = resource.properties[prop_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (created_at) {
|
||||||
|
new_properties['created_at'] = created_at;
|
||||||
|
}
|
||||||
|
if (updated_at) {
|
||||||
|
new_properties['updated_at'] = updated_at;
|
||||||
|
}
|
||||||
|
resource.properties = new_properties
|
||||||
|
}
|
||||||
|
|
||||||
private is_object(prop: any) {
|
private is_object(prop: any) {
|
||||||
return prop.hasOwnProperty('properties')
|
return prop.hasOwnProperty('properties')
|
||||||
}
|
}
|
||||||
@@ -140,6 +162,14 @@ export class JsonschemasService {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (this.is_enum(resource)) {
|
||||||
|
for (const ref of resource.allOf!) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (this.has_descendant(ref, property_name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error("Jsonschema format not implemented in property finder");
|
throw new Error("Jsonschema format not implemented in property finder");
|
||||||
return false;
|
return false;
|
||||||
@@ -159,7 +189,15 @@ export class JsonschemasService {
|
|||||||
} else if (this.is_union(resource)) {
|
} else if (this.is_union(resource)) {
|
||||||
for (const ref of resource.oneOf!) {
|
for (const ref of resource.oneOf!) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (this.has_property(ref, property_name)) {
|
if (this.has_descendant(ref, property_name)) {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.get_descendant(ref, property_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.is_enum(resource)) {
|
||||||
|
for (const ref of resource.allOf!) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (this.has_descendant(ref, property_name)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this.get_descendant(ref, property_name);
|
return this.get_descendant(ref, property_name);
|
||||||
}
|
}
|
||||||
@@ -184,6 +222,21 @@ export class JsonschemasService {
|
|||||||
path.substring(pointFirstPosition + 1)
|
path.substring(pointFirstPosition + 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_property_by_path(resource: JSONSchema7, path: string): JSONSchema7 {
|
||||||
|
const pointFirstPosition = path.indexOf('.')
|
||||||
|
if (pointFirstPosition == -1) {
|
||||||
|
return this.get_descendant(resource, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.get_property_by_path(
|
||||||
|
this.get_descendant(
|
||||||
|
resource,
|
||||||
|
path.substring(0, pointFirstPosition)
|
||||||
|
),
|
||||||
|
path.substring(pointFirstPosition + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
|
|||||||
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>
|
<form>
|
||||||
|
<button class="btn btn-success btn-lg float-end" type="button" i18n (click)="onCreate()">
|
||||||
<button class="btn btn-success btn-lg float-end" type="button" (click)="onCreate()">
|
|
||||||
Create {{ this.schema }}
|
Create {{ this.schema }}
|
||||||
</button>
|
</button>
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
<label for="table-complete-search" class="col-xs-3 col-sm-auto col-form-label">Full text search:</label>
|
<label for="table-complete-search" i18n class="col-xs-3 col-sm-auto col-form-label">Full text search:</label>
|
||||||
<div class="col-xs-3 col-sm-auto">
|
<div class="col-xs-3 col-sm-auto">
|
||||||
<input
|
<input
|
||||||
id="table-complete-search"
|
id="table-complete-search"
|
||||||
@@ -14,35 +13,45 @@
|
|||||||
[(ngModel)]="searchTerm"
|
[(ngModel)]="searchTerm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="col col-form-label" *ngIf="loading$ | async">Loading...</span>
|
<div class="col-xs-3 col-sm-auto">
|
||||||
|
<crud-list-filter-list
|
||||||
|
[filters]="this.filters"
|
||||||
|
[schema]="this.schema!"
|
||||||
|
[values]="this.searchFilters"
|
||||||
|
(filterChange)="onFilterChange($event)"
|
||||||
|
></crud-list-filter-list>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive-md">
|
<div class="table-responsive-md">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col }}</th>
|
<th *ngFor="let col of this.displayedColumns" scope="col" sortable="name" (sort)="onSort($event)">{{ col.title }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let row of listData$ | async" (click)="onSelect(row._id)">
|
<tr *ngIf="loading$ | async">
|
||||||
<td *ngFor="let col of this.displayedColumns">
|
<td class="text-center" [attr.colspan]="this.displayedColumns.length" i18n>Loading...</td>
|
||||||
<ngb-highlight [result]="getColumnValue(row,col)" [term]="searchTerm"></ngb-highlight>
|
</tr>
|
||||||
|
<tr *ngFor="let row of listData$ | async" (click)="onRowClick(row._id)" (auxclick)="onRowMiddleClick(row._id);" class="table-row-link">
|
||||||
|
<td class="text-truncate" *ngFor="let col of this.displayedColumns" style="max-width: 150px;">
|
||||||
|
<ngb-highlight [result]="getColumnValue(row, col.path)" [term]="searchTerm"></ngb-highlight>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between p-2">
|
<div class="d-flex justify-content-between p-2" *ngIf="! (loading$ | async)" >
|
||||||
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize">
|
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="page" [pageSize]="pageSize">
|
||||||
</ngb-pagination>
|
</ngb-pagination>
|
||||||
|
|
||||||
<select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="pageSize">
|
<select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="pageSize">
|
||||||
<option [ngValue]="10">10 items per page</option>
|
<option i18n [ngValue]="10">10 items per page</option>
|
||||||
<option [ngValue]="15">15 items per page</option>
|
<option i18n [ngValue]="15">15 items per page</option>
|
||||||
<option [ngValue]="25">25 items per page</option>
|
<option i18n [ngValue]="25">25 items per page</option>
|
||||||
<option [ngValue]="50">50 items per page</option>
|
<option i18n [ngValue]="50">50 items per page</option>
|
||||||
<option [ngValue]="100">100 items per page</option>
|
<option i18n [ngValue]="100">100 items per page</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -13,6 +13,12 @@ interface State {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
sortColumn: SortColumn;
|
sortColumn: SortColumn;
|
||||||
sortDirection: SortDirection;
|
sortDirection: SortDirection;
|
||||||
|
searchFilters: {[key: string]: any}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
path: string,
|
||||||
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,6 +29,7 @@ interface State {
|
|||||||
export class ListComponent implements OnInit {
|
export class ListComponent implements OnInit {
|
||||||
@Input() resource: string = "";
|
@Input() resource: string = "";
|
||||||
@Input() columns: string[] = [];
|
@Input() columns: string[] = [];
|
||||||
|
@Input() filters: string[] = [];
|
||||||
@Input() schema: string | undefined;
|
@Input() schema: string | undefined;
|
||||||
|
|
||||||
@Output() error: EventEmitter<string> = new EventEmitter();
|
@Output() error: EventEmitter<string> = new EventEmitter();
|
||||||
@@ -30,10 +37,9 @@ export class ListComponent implements OnInit {
|
|||||||
|
|
||||||
@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader> = new QueryList<NgbdSortableHeader>();
|
@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader> = new QueryList<NgbdSortableHeader>();
|
||||||
|
|
||||||
public displayedColumns: string[] = [];
|
public displayedColumns: Column[] = [];
|
||||||
|
|
||||||
private _loading$ = new BehaviorSubject<boolean>(true);
|
private _loading$ = new BehaviorSubject<boolean>(true);
|
||||||
//private _search$ = new Subject<void>();
|
|
||||||
private _listData$ = new BehaviorSubject<any[]>([]);
|
private _listData$ = new BehaviorSubject<any[]>([]);
|
||||||
private _total$ = new BehaviorSubject<number>(0);
|
private _total$ = new BehaviorSubject<number>(0);
|
||||||
|
|
||||||
@@ -43,6 +49,7 @@ export class ListComponent implements OnInit {
|
|||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
sortColumn: '_id',
|
sortColumn: '_id',
|
||||||
sortDirection: 'asc',
|
sortDirection: 'asc',
|
||||||
|
searchFilters: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private service: CrudService,
|
constructor(private service: CrudService,
|
||||||
@@ -56,37 +63,62 @@ export class ListComponent implements OnInit {
|
|||||||
next: (schema: any) => this.getColumnDefinition(schema),
|
next: (schema: any) => this.getColumnDefinition(schema),
|
||||||
error: (err) => this.error.emit("Error loading the schema:" + err)
|
error: (err) => this.error.emit("Error loading the schema:" + err)
|
||||||
});
|
});
|
||||||
this._search();
|
this.route.queryParams.subscribe(params => {
|
||||||
|
let parsedParams = {...params};
|
||||||
|
if (parsedParams.hasOwnProperty('searchFilters')) {
|
||||||
|
parsedParams['searchFilters'] = JSON.parse(parsedParams['searchFilters']);
|
||||||
|
}
|
||||||
|
this._set(parsedParams)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnDefinition(schema: JSONSchema7) {
|
getColumnDefinition(schema: JSONSchema7) {
|
||||||
for (let column of this.columns) {
|
for (let column of this.columns) {
|
||||||
if (this.jsonSchemasService.path_exists(schema, column)) {
|
if (this.jsonSchemasService.path_exists(schema, column)) {
|
||||||
this.displayedColumns.push(column);
|
this.displayedColumns.push({
|
||||||
|
path: column,
|
||||||
|
title: this.jsonSchemasService.get_property_by_path(schema, column).title!
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.displayedColumns.length == 0) {
|
if (this.displayedColumns.length == 0) {
|
||||||
for (let param_name in schema.properties) {
|
for (let param_name in schema.properties) {
|
||||||
if (param_name != "_id") {
|
if (param_name != "_id") {
|
||||||
this.displayedColumns.push(param_name);
|
this.displayedColumns.push({
|
||||||
|
path: param_name,
|
||||||
|
title: this.jsonSchemasService.get_property_by_path(schema, param_name).title!
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnValue(row: any, col: string) {
|
getColumnValue(row: any, col: string): string {
|
||||||
let parent = row;
|
let parent = row;
|
||||||
for (const key of col.split('.')) {
|
for (const key of col.split('.')) {
|
||||||
|
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];
|
parent = parent[key];
|
||||||
}
|
}
|
||||||
return parent;
|
}
|
||||||
|
return parent.replace(/<[^>]*>/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _search() {
|
private _search() {
|
||||||
this._loading$.next(true);
|
this._loading$.next(true);
|
||||||
let sortBy = new SortBy(this.sortColumn, this.sortDirection)
|
let sortBy = new SortBy(this.sortColumn, this.sortDirection)
|
||||||
let filters = this.searchTerm ? [new Filters('fulltext', 'eq', this.searchTerm)] : [];
|
let filters = this.searchTerm ? [new Filters('fulltext', 'eq', this.searchTerm)] : [];
|
||||||
|
for (let f in this.searchFilters) {
|
||||||
|
if (Array.isArray(this.searchFilters[f])) {
|
||||||
|
filters.push(new Filters(f, 'in', this.searchFilters[f]))
|
||||||
|
} else {
|
||||||
|
filters.push(new Filters(f, 'eq', this.searchFilters[f]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.service.getList(this.resource, this.page, this.pageSize, [sortBy], filters).subscribe({
|
this.service.getList(this.resource, this.page, this.pageSize, [sortBy], filters).subscribe({
|
||||||
next: (data: any) => {
|
next: (data: any) => {
|
||||||
@@ -101,6 +133,10 @@ export class ListComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFilterChange(event: any) {
|
||||||
|
this.searchFilters = event;
|
||||||
|
}
|
||||||
|
|
||||||
onSort({ column, direction }: any) {
|
onSort({ column, direction }: any) {
|
||||||
// resetting other headers
|
// resetting other headers
|
||||||
this.headers.forEach((header) => {
|
this.headers.forEach((header) => {
|
||||||
@@ -113,10 +149,15 @@ export class ListComponent implements OnInit {
|
|||||||
this.sortDirection = direction;
|
this.sortDirection = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect(id: string) {
|
onRowClick(id: string) {
|
||||||
this.router.navigate([`../${id}`], {relativeTo: this.route});
|
this.router.navigate([`../${id}`], {relativeTo: this.route});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRowMiddleClick(id: string) {
|
||||||
|
let newUrl = window.location.href.replace('list', id).split('?')[0]
|
||||||
|
window.open(newUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.router.navigate([`../new`], {relativeTo: this.route});
|
this.router.navigate([`../new`], {relativeTo: this.route});
|
||||||
}
|
}
|
||||||
@@ -145,21 +186,40 @@ export class ListComponent implements OnInit {
|
|||||||
get searchTerm() {
|
get searchTerm() {
|
||||||
return this._state.searchTerm;
|
return this._state.searchTerm;
|
||||||
}
|
}
|
||||||
|
get searchFilters() {
|
||||||
|
return this._state.searchFilters;
|
||||||
|
}
|
||||||
|
|
||||||
set page(page: number) {
|
set page(page: number) {
|
||||||
this._set({ page });
|
this.updateState({ page });
|
||||||
}
|
}
|
||||||
|
|
||||||
set pageSize(pageSize: number) {
|
set pageSize(pageSize: number) {
|
||||||
this._set({ pageSize });
|
this.updateState({ pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
set searchTerm(searchTerm: string) {
|
set searchTerm(searchTerm: string) {
|
||||||
this._set({ searchTerm });
|
this.updateState({ searchTerm });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set searchFilters(searchFilters: {[key: string]: any}) {
|
||||||
|
this.updateState({ searchFilters: JSON.stringify(searchFilters) });
|
||||||
|
}
|
||||||
|
|
||||||
set sortColumn(sortColumn: SortColumn) {
|
set sortColumn(sortColumn: SortColumn) {
|
||||||
this._set({ sortColumn });
|
this.updateState({ sortColumn });
|
||||||
}
|
}
|
||||||
|
|
||||||
set sortDirection(sortDirection: SortDirection) {
|
set sortDirection(sortDirection: SortDirection) {
|
||||||
this._set({ sortDirection });
|
this.updateState({ sortDirection });
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateState(patch: any) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: patch ,
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _set(patch: Partial<State>) {
|
private _set(patch: Partial<State>) {
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import { FieldArrayType } from '@ngx-formly/core';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'formly-array-type',
|
selector: 'formly-array-type',
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div class="mb-3">
|
||||||
|
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
|
||||||
|
<ngb-panel id="ngb-panel-0">
|
||||||
|
<ng-template ngbPanelTitle>
|
||||||
<label *ngIf="props.label" class="form-label">{{ props.label }}</label>
|
<label *ngIf="props.label" class="form-label">{{ props.label }}</label>
|
||||||
<p *ngIf="props.description">{{ props.description }}</p>
|
<p *ngIf="props.description">{{ props.description }}</p>
|
||||||
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
||||||
<formly-validation-message [field]="field"></formly-validation-message>
|
<formly-validation-message [field]="field"></formly-validation-message>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ngbPanelContent>
|
||||||
<div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1">
|
<div class="row row-cols-1 row-cols-md-{{this.itemsPerRow}} g-1">
|
||||||
<div *ngFor="let entry of field.fieldGroup; let i = index" class="col">
|
<div *ngFor="let entry of field.fieldGroup; let i = index" class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -17,7 +22,7 @@ import { FieldArrayType } from '@ngx-formly/core';
|
|||||||
<div *ngIf="props['numbered']" class="float-start">
|
<div *ngIf="props['numbered']" class="float-start">
|
||||||
<strong>{{ i + 1 }}</strong>
|
<strong>{{ i + 1 }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group float-end">
|
<div *ngIf="! this.field.props.readonly" class="btn-group float-end">
|
||||||
<button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button>
|
<button class="btn btn-primary btn-sm" [attr.disabled]="i == 0 ? 'disabled' : null" type="button" (click)="move(i, i-1)"><i-bs name="caret-up-fill"></i-bs></button>
|
||||||
<button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button>
|
<button class="btn btn-primary btn-sm" [attr.disabled]="i == this.field.fieldGroup!.length-1 ? 'disabled' : null" type="button" (click)="move(i, i+1)"><i-bs name="caret-down-fill"></i-bs></button>
|
||||||
<button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button>
|
<button class="btn btn-danger btn-sm" [attr.disabled]="field.props!['removable'] === false ? 'disabled' : null" type="button" (click)="remove(i)"><i-bs name="x-octagon-fill"></i-bs></button>
|
||||||
@@ -29,7 +34,10 @@ import { FieldArrayType } from '@ngx-formly/core';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-success col-sm-12" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
|
<button *ngIf="! this.field.props.readonly" class="btn btn-success col-sm-12 gap-3" type="button" (click)="add()"><i-bs name="plus-square-fill"></i-bs></button>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-panel>
|
||||||
|
</ngb-accordion>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
|
||||||
import { formatDate } from "@angular/common";
|
import { formatDate } from "@angular/common";
|
||||||
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
||||||
|
|
||||||
|
|
||||||
@@ -15,12 +14,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
|||||||
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
||||||
<formly-validation-message [field]="field"></formly-validation-message>
|
<formly-validation-message [field]="field"></formly-validation-message>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group" *ngIf="! this.field.props.readonly">
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
[formControl]="formControl"
|
[formControl]="formControl"
|
||||||
[formlyAttributes]="field"
|
[formlyAttributes]="field"
|
||||||
[class.is-invalid]="showError"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="input-group" *ngIf="! this.field.props.readonly">
|
||||||
|
<button class="btn btn-outline-secondary" (click)="d.toggle()" type="button"><i-bs name="calendar-date-fill"></i-bs></button>
|
||||||
<input
|
<input
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="yyyy-mm-dd"
|
placeholder="yyyy-mm-dd"
|
||||||
@@ -29,33 +28,40 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
|||||||
(ngModelChange)="changeDatetime($event)"
|
(ngModelChange)="changeDatetime($event)"
|
||||||
ngbDatepicker
|
ngbDatepicker
|
||||||
#d="ngbDatepicker"
|
#d="ngbDatepicker"
|
||||||
|
[class.is-invalid]="showError"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-outline-secondary" (click)="d.toggle()" type="button"><i-bs name="calendar-date-fill"></i-bs></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group" *ngIf="this.field.props.readonly">
|
<div class="input-group" *ngIf="this.field.props.readonly">
|
||||||
<input class="form-control" value="{{ this.datetime.toLocaleString() }}" disabled=""/>
|
<input class="form-control" value="{{ this.datetime ? this.datetime.toLocaleString() : '' }}" disabled=""/>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
|
export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
|
||||||
{
|
{
|
||||||
public date : NgbDateStruct;
|
public date : NgbDateStruct | null = null;
|
||||||
public datetime : Date = new Date();
|
public datetime : Date | null = null;
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.date = this.getDateStruct(new Date());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.formControl.value === undefined) {
|
if (this.formControl.value === undefined) {
|
||||||
this.changeDatetime({});
|
this.changeDatetime({});
|
||||||
|
} else {
|
||||||
|
this.datetime = new Date(this.formControl.value);
|
||||||
|
this.date = this.getDateStruct(this.datetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formControl.valueChanges.subscribe(value => {
|
this.formControl.valueChanges.subscribe(value => {
|
||||||
this.datetime = new Date(value)
|
if (value) {
|
||||||
|
this.datetime = new Date(value);
|
||||||
this.date = this.getDateStruct(this.datetime);
|
this.date = this.getDateStruct(this.datetime);
|
||||||
|
} else {
|
||||||
|
this.datetime = null;
|
||||||
|
this.date = null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +74,10 @@ export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeDatetime(event: any) {
|
changeDatetime(event: any) {
|
||||||
|
if (this.date) {
|
||||||
|
if (!this.datetime) {
|
||||||
|
this.datetime = new Date();
|
||||||
|
}
|
||||||
this.datetime.setFullYear(this.date.year)
|
this.datetime.setFullYear(this.date.year)
|
||||||
this.datetime.setMonth(this.date.month - 1)
|
this.datetime.setMonth(this.date.month - 1)
|
||||||
this.datetime.setDate(this.date.day)
|
this.datetime.setDate(this.date.day)
|
||||||
@@ -75,5 +85,10 @@ export class DateTypeComponent extends FieldType<FieldTypeConfig> implements OnI
|
|||||||
this.formControl.setValue(
|
this.formControl.setValue(
|
||||||
formatDate(this.datetime, 'YYYY-MM-dd', 'EN_US', 'CET')
|
formatDate(this.datetime, 'YYYY-MM-dd', 'EN_US', 'CET')
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
this.datetime = null;
|
||||||
|
this.date = null;
|
||||||
|
this.formControl.setValue('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
|
||||||
import { formatDate } from "@angular/common";
|
import { formatDate } from "@angular/common";
|
||||||
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
||||||
@@ -12,12 +11,12 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
|||||||
class="form-label">{{ props.label }}
|
class="form-label">{{ props.label }}
|
||||||
<span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span>
|
<span *ngIf="props.required && props['hideRequiredMarker'] !== true" aria-hidden="true">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group" *ngIf="! this.field.props.readonly">
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
[formControl]="formControl"
|
[formControl]="formControl"
|
||||||
[formlyAttributes]="field"
|
[formlyAttributes]="field"
|
||||||
[class.is-invalid]="showError"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="input-group" *ngIf="! this.field.props.readonly">
|
||||||
|
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
|
||||||
<input
|
<input
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="yyyy-mm-dd"
|
placeholder="yyyy-mm-dd"
|
||||||
@@ -26,8 +25,8 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
|||||||
(ngModelChange)="changeDatetime($event)"
|
(ngModelChange)="changeDatetime($event)"
|
||||||
ngbDatepicker
|
ngbDatepicker
|
||||||
#d="ngbDatepicker"
|
#d="ngbDatepicker"
|
||||||
|
[class.is-invalid]="showError"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="d.toggle()" type="button"></button>
|
|
||||||
<ngb-timepicker
|
<ngb-timepicker
|
||||||
(ngModelChange)="changeDatetime($event)"
|
(ngModelChange)="changeDatetime($event)"
|
||||||
[(ngModel)]="time"
|
[(ngModel)]="time"
|
||||||
@@ -35,15 +34,15 @@ import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
|
|||||||
</ngb-timepicker>
|
</ngb-timepicker>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group" *ngIf="this.field.props.readonly">
|
<div class="input-group" *ngIf="this.field.props.readonly">
|
||||||
<input class="form-control" value="{{ this.datetime.toLocaleString() }}" disabled=""/>
|
<input class="form-control" value="{{ this.datetime ? this.datetime.toLocaleString() : '' }}" disabled=""/>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
|
export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements OnInit
|
||||||
{
|
{
|
||||||
public time : NgbTimeStruct;
|
public time : NgbTimeStruct | null = null;
|
||||||
public date : NgbDateStruct;
|
public date : NgbDateStruct | null = null;
|
||||||
public datetime : Date = new Date()
|
public datetime : Date | null = null;
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -55,12 +54,21 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.formControl.value === undefined) {
|
if (this.formControl.value === undefined) {
|
||||||
this.changeDatetime({});
|
this.changeDatetime({});
|
||||||
|
} else {
|
||||||
|
this.datetime = new Date(this.formControl.value);
|
||||||
|
this.date = this.getDateStruct(this.datetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formControl.valueChanges.subscribe(value => {
|
this.formControl.valueChanges.subscribe(value => {
|
||||||
this.datetime = new Date(value)
|
if (value) {
|
||||||
|
this.datetime = new Date(value);
|
||||||
this.date = this.getDateStruct(this.datetime);
|
this.date = this.getDateStruct(this.datetime);
|
||||||
this.time = this.getTimeStruct(this.datetime);
|
this.time = this.getTimeStruct(this.datetime);
|
||||||
|
} else {
|
||||||
|
this.datetime = null;
|
||||||
|
this.date = null;
|
||||||
|
this.time = null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +89,10 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeDatetime(event: any) {
|
changeDatetime(event: any) {
|
||||||
|
if (this.date && this.time) {
|
||||||
|
if (!this.datetime) {
|
||||||
|
this.datetime = new Date();
|
||||||
|
}
|
||||||
this.datetime.setFullYear(this.date.year)
|
this.datetime.setFullYear(this.date.year)
|
||||||
this.datetime.setMonth(this.date.month - 1)
|
this.datetime.setMonth(this.date.month - 1)
|
||||||
this.datetime.setDate(this.date.day)
|
this.datetime.setDate(this.date.day)
|
||||||
@@ -91,5 +103,11 @@ export class DatetimeTypeComponent extends FieldType<FieldTypeConfig> implements
|
|||||||
this.formControl.setValue(
|
this.formControl.setValue(
|
||||||
formatDate(this.datetime, 'YYYY-MM-ddTHH:mm:ss.SSS', 'EN_US', 'CET')
|
formatDate(this.datetime, 'YYYY-MM-ddTHH:mm:ss.SSS', 'EN_US', 'CET')
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
this.datetime = null;
|
||||||
|
this.date = null;
|
||||||
|
this.time = null;
|
||||||
|
this.formControl.setValue('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {FormlyJsonschema} from "@ngx-formly/core/json-schema";
|
|||||||
template: `
|
template: `
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
|
<ngb-accordion #acc="ngbAccordion" activeIds="ngb-panel-0">
|
||||||
<ngb-panel>
|
<ngb-panel id="ngb-panel-0">
|
||||||
<ng-template ngbPanelTitle>
|
<ng-template ngbPanelTitle>
|
||||||
<label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id"
|
<label *ngIf="props.label && props['hideLabel'] !== true" [attr.for]="id"
|
||||||
class="form-label">{{ props.label }}
|
class="form-label">{{ props.label }}
|
||||||
|
|||||||
@@ -224,9 +224,11 @@ export class ForeignkeyTypeComponent extends FieldType<FieldTypeConfig> implemen
|
|||||||
|
|
||||||
result = result.concat();
|
result = result.concat();
|
||||||
} else if (typeof(obj[k]) == "object") {
|
} else if (typeof(obj[k]) == "object") {
|
||||||
|
if (obj[k]) {
|
||||||
result = result.concat(this.extractParameters(obj[k]));
|
result = result.concat(this.extractParameters(obj[k]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {Component, OnInit} from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { FormlyFieldInput } from "@ngx-formly/bootstrap/input";
|
import { FormlyFieldInput } from "@ngx-formly/bootstrap/input";
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'formly-richtext-type',
|
selector: 'formly-richtext-type',
|
||||||
template: `
|
template: `
|
||||||
@@ -42,11 +43,20 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
|
|||||||
statusbar: false,
|
statusbar: false,
|
||||||
autoresize_bottom_margin: 0,
|
autoresize_bottom_margin: 0,
|
||||||
body_class: "contract-body",
|
body_class: "contract-body",
|
||||||
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }"
|
content_style: ".contract-body { font-family: 'Century Schoolbook', 'sans-serif' }",
|
||||||
|
entity_encoding: 'raw',
|
||||||
|
paste_preprocess: function (plugin: any, args: any) {
|
||||||
|
console.log(args.content)
|
||||||
|
let container = document.createElement('div');
|
||||||
|
container.innerHTML = args.content.trim();
|
||||||
|
cleanPastedElement(container)
|
||||||
|
console.log(container.innerHTML);
|
||||||
|
args.content = container.innerHTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init_multiline = {
|
init_multiline = {
|
||||||
plugins: 'lists image imagetools table code searchreplace autoresize',
|
plugins: 'lists image imagetools table code searchreplace paste autoresize',
|
||||||
menubar: 'edit insert format tools table',
|
menubar: 'edit insert format tools table',
|
||||||
menu: {
|
menu: {
|
||||||
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
|
edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' },
|
||||||
@@ -59,7 +69,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init_singleline = {
|
init_singleline = {
|
||||||
plugins: 'autoresize',
|
plugins: 'paste autoresize',
|
||||||
menubar: '',
|
menubar: '',
|
||||||
toolbar: 'undo redo | bold italic underline',
|
toolbar: 'undo redo | bold italic underline',
|
||||||
}
|
}
|
||||||
@@ -71,7 +81,7 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitConfig() {
|
getInitConfig(): any {
|
||||||
return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)};
|
return {...this.init_common, ...( this.multiline ? this.init_multiline : this.init_singleline)};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +102,49 @@ export class RichtextTypeComponent extends FormlyFieldInput implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanPastedElement(htmlElement: HTMLElement): string {
|
||||||
|
if (! htmlElement.innerHTML) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let innerHtml = ""
|
||||||
|
for(let i = 0; i < htmlElement.childNodes.length; i++){
|
||||||
|
const childNode = htmlElement.childNodes[i] as HTMLElement
|
||||||
|
if (childNode.nodeName == "#text") {
|
||||||
|
innerHtml += childNode.nodeValue;
|
||||||
|
} else {
|
||||||
|
innerHtml += cleanPastedElement(childNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
htmlElement.innerHTML = innerHtml
|
||||||
|
|
||||||
|
if (htmlElement.tagName == "SPAN") {
|
||||||
|
let text = htmlElement.innerHTML
|
||||||
|
const style = htmlElement.style
|
||||||
|
|
||||||
|
if (style.fontWeight == "700") {
|
||||||
|
let strong = document.createElement('b');
|
||||||
|
strong.innerHTML = text
|
||||||
|
text = strong.outerHTML;
|
||||||
|
}
|
||||||
|
if (style.textDecoration == "underline") {
|
||||||
|
let underline = document.createElement('u');
|
||||||
|
underline.innerHTML = text;
|
||||||
|
text = underline.outerHTML;
|
||||||
|
}
|
||||||
|
if (style.fontStyle == "italic") {
|
||||||
|
let italic = document.createElement('em');
|
||||||
|
italic.innerHTML = text;
|
||||||
|
text = italic.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.style.removeProperty("line-height")
|
||||||
|
htmlElement.style.removeProperty("margin")
|
||||||
|
|
||||||
|
return htmlElement.outerHTML
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 318 B |
@@ -2,10 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>App</title>
|
<title>Cooper, Hillman & Toshi</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<meta property="og:title" content="Cooper, Hillman & Toshi">
|
||||||
|
<meta property="og:description" content="Interface d'administration des contrats">
|
||||||
|
<meta property="og:image" content="/assets/logo.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
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,15 @@
|
|||||||
.contract-body {
|
.contract-body {
|
||||||
font-family: 'Century Schoolbook';
|
font-family: 'Century Schoolbook';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: #D2BA6F;
|
||||||
|
--bs-nav-link-hover-color: #9c8a51;
|
||||||
|
--bs-nav-pills-link-active-color: #D2BA6F;
|
||||||
|
--bs-nav-pills-link-active-bg: #114856;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
border: #D2BA6F solid 2px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|||||||
72
front/nginx.prod.conf
Normal file
72
front/nginx.prod.conf
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
|
||||||
|
events { worker_connections 1024; }
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
upstream docker-back {
|
||||||
|
server back:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
types {
|
||||||
|
module js;
|
||||||
|
}
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
gzip_comp_level 9;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html?$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^.+\.css$ {
|
||||||
|
default_type text/css;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^.+\.js$ {
|
||||||
|
default_type text/javascript;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /contracts/signature/ {
|
||||||
|
set $is_robot 0;
|
||||||
|
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
|
||||||
|
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
|
||||||
|
proxy_pass http://docker-back;
|
||||||
|
set $is_robot 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_robot = 0) {
|
||||||
|
rewrite ^ /index.html?$args last;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://docker-back/;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,26 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Host $server_name;
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /contracts/signature/ {
|
||||||
|
set $is_robot 0;
|
||||||
|
if ($http_user_agent ~* "Discordbot|googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/0\.|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp") {
|
||||||
|
rewrite /contracts/signature/(.*) /api/v1/contract/print/opengraph/$1 last;
|
||||||
|
proxy_pass http://docker-back;
|
||||||
|
break;
|
||||||
|
set $is_robot 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_robot = 0) {
|
||||||
|
proxy_pass http://docker-front;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $server_name;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/v1/ {
|
location /api/v1/ {
|
||||||
proxy_pass http://docker-back/;
|
proxy_pass http://docker-back/;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|||||||
Reference in New Issue
Block a user