add logout logic and wip recap
This commit is contained in:
@@ -6,9 +6,6 @@
|
||||
- Extract recap
|
||||
- Extract all contracts
|
||||
- store total price
|
||||
- store pdf file
|
||||
|
||||
- fix price display (reccurent in contract template)
|
||||
|
||||
## Wording
|
||||
|
||||
@@ -42,5 +39,3 @@
|
||||
## Update contract after register
|
||||
|
||||
## Default filter
|
||||
|
||||
## token expired refresh token
|
||||
|
||||
@@ -29,6 +29,7 @@ dependencies = [
|
||||
"cryptography",
|
||||
"requests",
|
||||
"weasyprint",
|
||||
"odfdo"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -13,7 +13,7 @@ from src.models import UserCreate, User, UserPublic
|
||||
|
||||
import secrets
|
||||
import requests
|
||||
|
||||
from urllib.parse import urlencode
|
||||
import src.messages as messages
|
||||
|
||||
router = APIRouter(prefix='/auth')
|
||||
@@ -23,24 +23,13 @@ security = HTTPBearer()
|
||||
|
||||
@router.get('/logout')
|
||||
def logout(
|
||||
id_token: Annotated[str | None, Cookie()] = None,
|
||||
refresh_token: Annotated[str | None, Cookie()] = None,
|
||||
):
|
||||
if refresh_token:
|
||||
print("invalidate tokens")
|
||||
requests.post(LOGOUT_URL, data={
|
||||
"client_id": settings.keycloak_client_id,
|
||||
"client_secret": settings.keycloak_client_secret,
|
||||
"refresh_token": refresh_token
|
||||
})
|
||||
|
||||
if id_token:
|
||||
print("redirect keycloak")
|
||||
response = RedirectResponse(f'{LOGOUT_URL}?post_logout_redirect_uri={settings.origins}&id_token_hint={id_token}')
|
||||
else:
|
||||
response = RedirectResponse(settings.origins)
|
||||
|
||||
print("clear cookies")
|
||||
params = {
|
||||
'client_id': settings.keycloak_client_id,
|
||||
'post_logout_redirect_uri': settings.origins,
|
||||
}
|
||||
response = RedirectResponse(f'{LOGOUT_URL}?{urlencode(params)}')
|
||||
response.delete_cookie(
|
||||
key='access_token',
|
||||
path='/',
|
||||
@@ -59,6 +48,12 @@ def logout(
|
||||
secure=not settings.debug,
|
||||
samesite='lax',
|
||||
)
|
||||
# if refresh_token:
|
||||
# requests.post(LOGOUT_URL, data={
|
||||
# 'client_id': settings.keycloak_client_id,
|
||||
# 'client_secret': settings.keycloak_client_secret,
|
||||
# 'refresh_token': refresh_token
|
||||
# })
|
||||
return response
|
||||
|
||||
|
||||
@@ -107,9 +102,9 @@ def callback(code: str, session: Session = Depends(get_session)):
|
||||
'refresh_token': token_data['refresh_token'],
|
||||
}
|
||||
res = requests.post(LOGOUT_URL, data=data)
|
||||
resp = RedirectResponse(settings.origins)
|
||||
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
|
||||
return resp
|
||||
resource_access.get(settings.keycloak_client_id)
|
||||
roles = resource_access.get(settings.keycloak_client_id)
|
||||
if not roles:
|
||||
data = {
|
||||
'client_id': settings.keycloak_client_id,
|
||||
@@ -117,7 +112,7 @@ def callback(code: str, session: Session = Depends(get_session)):
|
||||
'refresh_token': token_data['refresh_token'],
|
||||
}
|
||||
res = requests.post(LOGOUT_URL, data=data)
|
||||
resp = RedirectResponse(settings.origins)
|
||||
resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
|
||||
return resp
|
||||
|
||||
user_create = UserCreate(
|
||||
|
||||
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
from src.contracts.generate_contract import generate_html_contract
|
||||
from src.contracts.generate_contract import generate_html_contract, generate_recap
|
||||
from src.auth.auth import get_current_user
|
||||
import src.models as models
|
||||
import src.messages as messages
|
||||
@@ -79,7 +79,8 @@ async def create_contract(
|
||||
occasionals = create_occasional_dict(occasional_contract_products)
|
||||
recurrents = list(map(lambda x: {"product": x.product, "quantity": x.quantity}, filter(lambda contract_product: contract_product.product.type == models.ProductType.RECCURENT, new_contract.products)))
|
||||
recurrent_price = compute_recurrent_prices(recurrents, len(new_contract.form.shipments))
|
||||
total_price = '{:10.2f}'.format(recurrent_price + compute_occasional_prices(occasionals))
|
||||
price = recurrent_price + compute_occasional_prices(occasionals)
|
||||
total_price = '{:10.2f}'.format(price)
|
||||
cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
|
||||
# TODO: send contract to referer
|
||||
|
||||
@@ -94,7 +95,7 @@ async def create_contract(
|
||||
)
|
||||
pdf_file = io.BytesIO(pdf_bytes)
|
||||
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
|
||||
service.add_contract_file(session, new_contract.id, pdf_bytes)
|
||||
service.add_contract_file(session, new_contract.id, pdf_bytes, price)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=400, detail=messages.pdferror)
|
||||
@@ -112,7 +113,7 @@ def get_contracts(
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
return service.get_all(session, forms)
|
||||
return service.get_all(session, user, forms)
|
||||
|
||||
@router.get('/{id}/file')
|
||||
def get_contract_file(
|
||||
@@ -120,6 +121,8 @@ def get_contract_file(
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if not service.is_allowed(session, user, id):
|
||||
raise HTTPException(status_code=403, detail=messages.notallowed)
|
||||
contract = service.get_one(session, id)
|
||||
if contract is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
@@ -138,8 +141,10 @@ def get_contract_files(
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if not form_service.is_allowed(session, user, form_id):
|
||||
raise HTTPException(status_code=403, detail=messages.notallowed)
|
||||
form = form_service.get_one(session, form_id=form_id)
|
||||
contracts = service.get_all(session, [form.name])
|
||||
contracts = service.get_all(session, user, forms=[form.name])
|
||||
zipped_contracts = io.BytesIO()
|
||||
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
||||
for contract in contracts:
|
||||
@@ -155,9 +160,29 @@ def get_contract_files(
|
||||
}
|
||||
)
|
||||
|
||||
@router.get('/{form_id}/recap')
|
||||
def get_contract_recap(
|
||||
form_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
if not form_service.is_allowed(session, user, form_id):
|
||||
raise HTTPException(status_code=403, detail=messages.notallowed)
|
||||
form = form_service.get_one(session, form_id=form_id)
|
||||
contracts = service.get_all(session, user, forms=[form.name])
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(generate_recap(contracts, form)),
|
||||
media_type='application/zip',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename=filename.ods'
|
||||
}
|
||||
)
|
||||
|
||||
@router.get('/{id}', response_model=models.ContractPublic)
|
||||
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
|
||||
if not service.is_allowed(session, user, id):
|
||||
raise HTTPException(status_code=403, detail=messages.notallowed)
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
@@ -165,6 +190,8 @@ def get_contract(id: int, session: Session = Depends(get_session), user: models.
|
||||
|
||||
@router.delete('/{id}', response_model=models.ContractPublic)
|
||||
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
|
||||
if not service.is_allowed(session, user, id):
|
||||
raise HTTPException(status_code=403, detail=messages.notallowed)
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -3,6 +3,7 @@ import jinja2
|
||||
import src.models as models
|
||||
import html
|
||||
from weasyprint import HTML
|
||||
import io
|
||||
|
||||
def generate_html_contract(
|
||||
contract: models.Contract,
|
||||
@@ -57,4 +58,25 @@ def generate_html_contract(
|
||||
return HTML(
|
||||
string=output_text,
|
||||
base_url=template_dir
|
||||
).write_pdf()
|
||||
).write_pdf()
|
||||
|
||||
from odfdo import Document, Table, Row, Cell
|
||||
|
||||
def generate_recap(
|
||||
contracts: list[models.Contract],
|
||||
form: models.Form,
|
||||
):
|
||||
data = [
|
||||
["nom", "email"],
|
||||
]
|
||||
doc = Document("spreadsheet")
|
||||
sheet = Table(name="Recap")
|
||||
sheet.set_values(data)
|
||||
|
||||
doc.body.append(sheet)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
doc.save(buffer)
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
@@ -3,14 +3,19 @@ import src.models as models
|
||||
|
||||
def get_all(
|
||||
session: Session,
|
||||
user: models.User,
|
||||
forms: list[str] = [],
|
||||
form_id: int | None = None,
|
||||
form_id: int | None = None,
|
||||
) -> list[models.ContractPublic]:
|
||||
statement = select(models.Contract)
|
||||
if form_id:
|
||||
statement = statement.join(models.Form).where(models.Form.id == form_id)
|
||||
statement = select(models.Contract)\
|
||||
.join(models.Form, models.Contract.form_id == models.Form.id)\
|
||||
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
|
||||
.where(models.Productor.type.in_([r.name for r in user.roles]))\
|
||||
.distinct()
|
||||
if len(forms) > 0:
|
||||
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
|
||||
statement = statement.where(models.Form.name.in_(forms))
|
||||
if form_id:
|
||||
statement = statement.where(models.Form.id == form_id)
|
||||
return session.exec(statement.order_by(models.Contract.id)).all()
|
||||
|
||||
def get_one(session: Session, contract_id: int) -> models.ContractPublic:
|
||||
@@ -42,11 +47,11 @@ def create_one(session: Session, contract: models.ContractCreate) -> models.Cont
|
||||
session.refresh(new_contract)
|
||||
return new_contract
|
||||
|
||||
def add_contract_file(session: Session, id: int, file: bytes):
|
||||
def add_contract_file(session: Session, id: int, file: bytes, price: float):
|
||||
statement = select(models.Contract).where(models.Contract.id == id)
|
||||
result = session.exec(statement)
|
||||
contract = result.first()
|
||||
|
||||
contract.total_price = price
|
||||
contract.file = file
|
||||
session.add(contract)
|
||||
session.commit()
|
||||
@@ -77,3 +82,12 @@ def delete_one(session: Session, id: int) -> models.ContractPublic:
|
||||
session.delete(contract)
|
||||
session.commit()
|
||||
return result
|
||||
|
||||
def is_allowed(session: Session, user: models.User, id: int) -> bool:
|
||||
statement = select(models.Contract)\
|
||||
.join(models.Form, models.Contract.form_id == models.Form.id)\
|
||||
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
|
||||
.where(models.Contract.id == id)\
|
||||
.where(models.Productor.type.in_([r.name for r in user.roles]))\
|
||||
.distinct()
|
||||
return len(session.exec(statement).all()) > 0
|
||||
@@ -12,9 +12,10 @@ router = APIRouter(prefix='/forms')
|
||||
async def get_forms(
|
||||
seasons: list[str] = Query([]),
|
||||
productors: list[str] = Query([]),
|
||||
current_season: bool = False,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.get_all(session, seasons, productors)
|
||||
return service.get_all(session, seasons, productors, current_season)
|
||||
|
||||
@router.get('/{id}', response_model=models.FormPublic)
|
||||
async def get_form(id: int, session: Session = Depends(get_session)):
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
from sqlmodel import Session, select
|
||||
import src.models as models
|
||||
from sqlalchemy import func
|
||||
|
||||
def get_all(
|
||||
session: Session,
|
||||
seasons: list[str],
|
||||
productors: list[str]
|
||||
productors: list[str],
|
||||
current_season: bool,
|
||||
) -> list[models.FormPublic]:
|
||||
statement = select(models.Form)
|
||||
if len(seasons) > 0:
|
||||
statement = statement.where(models.Form.season.in_(seasons))
|
||||
if len(productors) > 0:
|
||||
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
|
||||
if current_season:
|
||||
subquery = (
|
||||
select(
|
||||
models.Productor.type,
|
||||
func.max(models.Form.start).label("max_start")
|
||||
)
|
||||
.join(models.Form)\
|
||||
.group_by(models.Productor.type)\
|
||||
.subquery()
|
||||
)
|
||||
statement = select(models.Form)\
|
||||
.join(models.Productor)\
|
||||
.join(subquery,
|
||||
(models.Productor.type == subquery.c.type) &
|
||||
(models.Form.start == subquery.c.max_start)
|
||||
)
|
||||
return session.exec(statement.order_by(models.Form.name)).all()
|
||||
return session.exec(statement.order_by(models.Form.name)).all()
|
||||
|
||||
def get_one(session: Session, form_id: int) -> models.FormPublic:
|
||||
@@ -48,3 +67,11 @@ def delete_one(session: Session, id: int) -> models.FormPublic:
|
||||
session.delete(form)
|
||||
session.commit()
|
||||
return result
|
||||
|
||||
def is_allowed(session: Session, user: models.User, id: int) -> bool:
|
||||
statement = select(models.Form)\
|
||||
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
|
||||
.where(models.Form.id == id)\
|
||||
.where(models.Productor.type.in_([r.name for r in user.roles]))\
|
||||
.distinct()
|
||||
return len(session.exec(statement).all()) > 0
|
||||
@@ -6,4 +6,5 @@ notauthenticated = "Not authenticated"
|
||||
usernotfound = "User not found"
|
||||
userloggedout = "User logged out"
|
||||
failtogettoken = "Failed to get token"
|
||||
unauthorized = "Unauthorized"
|
||||
unauthorized = "Unauthorized"
|
||||
notallowed = "Not Allowed"
|
||||
@@ -222,6 +222,7 @@ class Contract(ContractBase, table=True):
|
||||
cascade_delete=True
|
||||
)
|
||||
file: bytes = Field(sa_column=Column(LargeBinary))
|
||||
total_price: float | None
|
||||
|
||||
class ContractCreate(ContractBase):
|
||||
products: list["ContractProductCreate"] = []
|
||||
@@ -235,6 +236,7 @@ class ContractPublic(ContractBase):
|
||||
id: int
|
||||
products: list["ContractProduct"] = []
|
||||
form: Form
|
||||
total_price: float | None
|
||||
# file: bytes
|
||||
|
||||
class ContractProductBase(SQLModel):
|
||||
|
||||
@@ -16,7 +16,7 @@ def get_all(
|
||||
def get_one(session: Session, user_id: int) -> models.UserPublic:
|
||||
return session.get(models.User, user_id)
|
||||
|
||||
def get_or_create_roles(session: Session, role_names) -> list[models.ContractType]:
|
||||
def get_or_create_roles(session: Session, role_names: list[str]) -> list[models.ContractType]:
|
||||
statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
|
||||
existing = session.exec(statement).all()
|
||||
existing_roles = {role.name for role in existing}
|
||||
@@ -37,6 +37,9 @@ def get_or_create_user(session: Session, user_create: models.UserCreate):
|
||||
statement = select(models.User).where(models.User.email == user_create.email)
|
||||
user = session.exec(statement).first()
|
||||
if user:
|
||||
user_role_names = [r.name for r in user.roles]
|
||||
if user_role_names != user_create.role_names or user.name != user_create.name:
|
||||
user = update_one(session, user.id, user_create)
|
||||
return user
|
||||
user = create_one(session, user_create)
|
||||
return user
|
||||
@@ -46,7 +49,6 @@ def get_roles(session: Session):
|
||||
return session.exec(statement.order_by(models.ContractType.name)).all()
|
||||
|
||||
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
|
||||
print("USER CREATE", user)
|
||||
new_user = models.User(
|
||||
name=user.name,
|
||||
email=user.email
|
||||
@@ -60,15 +62,20 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
|
||||
session.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
def update_one(session: Session, id: int, user: models.UserUpdate) -> models.UserPublic:
|
||||
def update_one(session: Session, id: int, user: models.UserCreate) -> models.UserPublic:
|
||||
statement = select(models.User).where(models.User.id == id)
|
||||
result = session.exec(statement)
|
||||
new_user = result.first()
|
||||
if not new_user:
|
||||
return None
|
||||
user_updates = user.model_dump(exclude_unset=True)
|
||||
|
||||
user_updates = user.model_dump(exclude="role_names")
|
||||
for key, value in user_updates.items():
|
||||
setattr(new_user, key, value)
|
||||
|
||||
roles = get_or_create_roles(session, user.role_names)
|
||||
new_user.roles = roles
|
||||
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
@@ -83,4 +90,4 @@ def delete_one(session: Session, id: int) -> models.UserPublic:
|
||||
result = models.UserPublic.model_validate(user)
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return result
|
||||
return result
|
||||
@@ -29,7 +29,7 @@
|
||||
"edit form": "edit contract form",
|
||||
"form name": "contract form name",
|
||||
"contract season": "contract season",
|
||||
"contract season recommandation": "recommendation: <Season>-<year> (Example: Winter-2025)",
|
||||
"contract season recommandation": "recommendation: <Season>-<year> (Example: Winter-2025), if a form is already created, reuse it's season name.",
|
||||
"start date": "start date",
|
||||
"end date": "end date",
|
||||
"nothing found": "nothing to display",
|
||||
@@ -58,6 +58,7 @@
|
||||
"quantity unit": "quantity unit",
|
||||
"unit": "sales unit",
|
||||
"price": "price",
|
||||
"total price": "total price",
|
||||
"create product": "create product",
|
||||
"informations": "information",
|
||||
"remove product": "remove product",
|
||||
@@ -72,6 +73,13 @@
|
||||
"shipment products is necessary only for occasional products (if all products are recurrent leave empty)": "shipment products configuration is only necessary for occasional products (leave empty if all products are recurrent).",
|
||||
"recurrent product is for all shipments, occasional product is for a specific shipment (see shipment form)": "recurrent products are for all shipments, occasional products are for a specific shipment (see shipment form).",
|
||||
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment. Ignore this field if it does not apply to your contract.",
|
||||
"export contracts": "export contracts",
|
||||
"download recap": "download recap",
|
||||
"to export contracts submissions before sending to the productor go to the contracts section": "to export contracts submissions before sending to the productor go to the contracts section.",
|
||||
"in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract": "in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract",
|
||||
"you can download all contracts for your form using the export all": "you can download all contracts for your form using the export all",
|
||||
"in the same corner you can download a recap by clicking on the button": "in the same corner you can download a recap by clicking on the",
|
||||
"once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page",
|
||||
"contracts": "contracts",
|
||||
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
|
||||
"there is": "there is",
|
||||
@@ -130,7 +138,7 @@
|
||||
"to add a use the": "to add {{section}} use the button",
|
||||
"to edit a use the": "to edit {{section}} use the button",
|
||||
"to delete a use the": "to delete {{section}} use the button",
|
||||
"button in top left of the page": "at the top left of the {{section}} page.",
|
||||
"button in top right of the page": "at the top right of the {{section}} page.",
|
||||
"button in front of the line you want to edit": "in front of the line you want to edit (in the actions column).",
|
||||
"button in front of the line you want to delete": "in front of the line you want to delete (in the actions column).",
|
||||
"glossary": "glossary",
|
||||
@@ -188,6 +196,10 @@
|
||||
"enter payment method": "select your payment method",
|
||||
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "number of cheques between 1 and 3. Also enter cheque identifiers.",
|
||||
"payment method": "payment method",
|
||||
"your session has expired please log in again": "your session has expired please log in again",
|
||||
"session expired": "session expired",
|
||||
"user not allowed": "user not allowed",
|
||||
"your keycloak user has no roles, please contact your administrator": "your keycloak user has no roles, please contact your administrator",
|
||||
"choose payment method": "choose your payment method (you do not need to pay now).",
|
||||
"the product unit will be assigned to the quantity requested in the form": "the product unit defines the unit used in the contract form.",
|
||||
"all theses informations are for contract generation": "all this information is required for contract generation."
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"edit form": "modifier le formulaire de contrat",
|
||||
"form name": "nom du formulaire de contrat",
|
||||
"contract season": "saison du contrat",
|
||||
"contract season recommandation": "recommandation : <Saison>-<année> (Exemple: Hiver-2025)",
|
||||
"contract season recommandation": "recommandation : <Saison>-<année> (Exemple: Hiver-2025), si un formulaire est déjà créé pour la saison, reprenez son nom de saison si possible.",
|
||||
"start date": "date de début",
|
||||
"end date": "date de fin",
|
||||
"nothing found": "rien à afficher",
|
||||
@@ -58,6 +58,7 @@
|
||||
"quantity unit": "unité de quantité",
|
||||
"unit": "unité de vente",
|
||||
"price": "prix",
|
||||
"total price": "prix total",
|
||||
"create product": "créer le produit",
|
||||
"informations": "informations",
|
||||
"remove product": "supprimer le produit",
|
||||
@@ -129,7 +130,7 @@
|
||||
"to add a use the": "pour ajouter {{section}} utilisez le bouton",
|
||||
"to edit a use the": "pour éditer {{section}} utilisez le bouton",
|
||||
"to delete a use the": "pour supprimer {{section}} utilisez le bouton",
|
||||
"button in top left of the page": "en haut à gauche de la page {{section}}.",
|
||||
"button in top right of the page": "en haut à droite de la page {{section}}.",
|
||||
"button in front of the line you want to edit": "en face de la ligne que vous souhaitez éditer. (dans la colonne actions).",
|
||||
"button in front of the line you want to delete": "en face de la ligne que vous souhaitez supprimer. (dans la colonne actions).",
|
||||
"glossary": "glossaire",
|
||||
@@ -137,8 +138,15 @@
|
||||
"start to create a productor in the productors section": "commencez par créer un(e) producteur·trice dans la section \"Producteur·trices\".",
|
||||
"add all products linked to this productor in the products section": "ajoutez vos produits liés au/à la producteur·trice dans la section \"Produits\".",
|
||||
"create your contract form, it will create a form in the home page (accessible to users)": "créez votre formulaire de contrat dans la section \"Formulaire de contrat\". Ajouter une entrée dans cette section ajoutera un formulaire dans la page d'accueil.",
|
||||
"export contracts": "Télécharger les contrats",
|
||||
"download recap": "Télécharger le récapitulatif",
|
||||
"in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract": "dans cette page vous pouvez voir tous les contrats, vous pouvez supprimer un contrat en doublon, ou télécharger uniquement un contrat.",
|
||||
"create shipments for your contract form": "créez les livraisons pour votre contrat",
|
||||
"creation order": "ordre de création",
|
||||
"in the same corner you can download a recap by clicking on the button": "au même endroit vous pouvez exporter votre récapitulatif (format odt) à vérifier et transmettre au producteur en cliquant sur le bouton",
|
||||
"to export contracts submissions before sending to the productor go to the contracts section": "pour exporter les contrats avant de les envoyer aux producteurs allez dans la section \"contrats\".",
|
||||
"once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "une fois tous les contrats récupérés vous pouvez supprimer le formulaire (pour éviter les nouvelles demandes de contrat et pour cacher le formulaire de la page principale).",
|
||||
"you can download all contracts for your form using the export all": "vous pouvez télécharger tous les contrats de votre formulaire en utilisant le bouton",
|
||||
"dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments": "le tableau de bord est visible uniquement pour les référents, vous pouvez créer votre producteur, vos produits, vos formulaires de contrat et vos livraisons.",
|
||||
"is defined by": "est defini par",
|
||||
"a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments": "un type de produit définit la manière dont un produit va être présenté aux amapiens dans le formulaire de contrat. Il peut être récurrent ou occasionnel. Un produit récurrent si selectionné sera compté pour toutes les livraisons. Un produit occasionnel sera facultatif pour chaques livraison (l'amapien devra selectionner la quantité voulue pour chaque livraisons).",
|
||||
@@ -188,6 +196,10 @@
|
||||
"enter payment method": "sélectionnez votre méthode de paiement",
|
||||
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "nombre de chèques entre 1 et 3, entrez également les identifiants des chèques utilisés.",
|
||||
"payment method": "méthode de paiement",
|
||||
"your session has expired please log in again": "votre session a expiré veuillez vous reconnecter.",
|
||||
"session expired": "session expirée",
|
||||
"user not allowed": "utilisateur non authorisé",
|
||||
"your keycloak user has no roles, please contact your administrator": "votre utilisateur keycloak n'a pas de roles configurés, contactez votre administrateur.",
|
||||
"choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).",
|
||||
"the product unit will be assigned to the quantity requested in the form": "l'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.",
|
||||
"all theses informations are for contract generation": "ces informations sont nécessaires pour la génération de contrat."
|
||||
|
||||
@@ -28,6 +28,14 @@ export default function ContractRow({ contract }: ContractRowProps) {
|
||||
<Table.Td>
|
||||
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
`${Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(contract.total_price)}`
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label={t("download contract", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
|
||||
@@ -41,7 +41,6 @@ export function Navbar() {
|
||||
href={`${Config.backend_uri}/auth/logout`}
|
||||
className={"navLink"}
|
||||
aria-label={t("logout", { capfirst: true })}
|
||||
|
||||
>
|
||||
{t("logout", { capfirst: true })}
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from "@/config/i18n";
|
||||
import type { ContractInputs } from "@/services/resources/contracts";
|
||||
import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { Group, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -56,13 +56,18 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
|
||||
{...inputForm.getInputProps(`cheque_quantity`)}
|
||||
/>
|
||||
<Group grow>
|
||||
{inputForm.values.cheques.map((_cheque, index) => (
|
||||
{inputForm.values.cheques.map((cheque, index) => (
|
||||
<Stack key={`${index}`}>
|
||||
<TextInput
|
||||
label={t("cheque id", { capfirst: true })}
|
||||
placeholder={t("cheque id", { capfirst: true })}
|
||||
{...inputForm.getInputProps(`cheques.${index}.name`)}
|
||||
/>
|
||||
error={
|
||||
cheque.name == "" ?
|
||||
<Text size="sm" c="red">{inputForm?.errors.cheques}</Text> :
|
||||
null
|
||||
}
|
||||
/>
|
||||
<NumberInput
|
||||
readOnly
|
||||
label={t("cheque value", { capfirst: true })}
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function ProductorsFilter({
|
||||
const defaultTypes = useMemo(() => {
|
||||
return filters.getAll("types");
|
||||
}, [filters]);
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<MultiSelect
|
||||
|
||||
@@ -46,6 +46,12 @@ export function ProductorModal({
|
||||
!value ? `${t("address", { capfirst: true })} ${t("is required")}` : null,
|
||||
type: (value) =>
|
||||
!value ? `${t("type", { capfirst: true })} ${t("is required")}` : null,
|
||||
payment_methods: (value) =>
|
||||
value.length === 0 || value.some(
|
||||
(payment) =>
|
||||
payment.name === "cheque" &&
|
||||
payment.details === "") ?
|
||||
`${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,6 +95,7 @@ export function ProductorModal({
|
||||
clearable
|
||||
searchable
|
||||
value={form.values.payment_methods.map((p) => p.name)}
|
||||
error={form.errors.payment_methods}
|
||||
onChange={(names) => {
|
||||
form.setFieldValue(
|
||||
"payment_methods",
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Contract() {
|
||||
email: "",
|
||||
phone: "",
|
||||
payment_method: "",
|
||||
cheque_quantity: 1,
|
||||
cheque_quantity: 0,
|
||||
cheques: [],
|
||||
products: {},
|
||||
},
|
||||
@@ -50,6 +50,8 @@ export function Contract() {
|
||||
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
|
||||
payment_method: (value) =>
|
||||
!value ? `${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
|
||||
cheques: (value, values) =>
|
||||
values.payment_method === "cheque" && value.some((val) => val.name == "") ? `${t("cheque id", {capfirst: true})} ${t("is required")}` : null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useGetAllContractFile, useGetContracts } from "@/services/api";
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import { useGetAllContractFile, useGetContracts, useGetRecap } from "@/services/api";
|
||||
import { IconDownload, IconTableExport } from "@tabler/icons-react";
|
||||
import ContractRow from "@/components/Contracts/Row";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { ContractModal } from "@/components/Contracts/Modal";
|
||||
@@ -14,7 +14,9 @@ export default function Contracts() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const getAllContractFilesMutation = useGetAllContractFile();
|
||||
const getRecapMutation = useGetRecap();
|
||||
const isdownload = location.pathname.includes("/download");
|
||||
const isrecap = location.pathname.includes("/export");
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
@@ -52,6 +54,13 @@ export default function Contracts() {
|
||||
[getAllContractFilesMutation],
|
||||
);
|
||||
|
||||
const handleDownloadRecap = useCallback(
|
||||
async (id: number) => {
|
||||
await getRecapMutation.mutateAsync(id);
|
||||
},
|
||||
[getAllContractFilesMutation],
|
||||
)
|
||||
|
||||
if (!contracts || isPending)
|
||||
return (
|
||||
<Group align="center" justify="center" h="80vh" w="100%">
|
||||
@@ -63,23 +72,45 @@ export default function Contracts() {
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>{t("all contracts", { capfirst: true })}</Title>
|
||||
<Tooltip label={t("download contracts", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
<Group>
|
||||
<Tooltip label={t("download contracts", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={t("download recap", { capfirst: true })}
|
||||
>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/export${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconTableExport />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<ContractModal
|
||||
opened={isdownload}
|
||||
onClose={closeModal}
|
||||
handleSubmit={handleDownloadContracts}
|
||||
/>
|
||||
<ContractModal
|
||||
opened={isrecap}
|
||||
onClose={closeModal}
|
||||
handleSubmit={handleDownloadRecap}
|
||||
/>
|
||||
</Group>
|
||||
<ContractsFilters
|
||||
forms={forms || []}
|
||||
@@ -94,6 +125,7 @@ export default function Contracts() {
|
||||
<Table.Th>{t("name", { capfirst: true })}</Table.Th>
|
||||
<Table.Th>{t("email", { capfirst: true })}</Table.Th>
|
||||
<Table.Th>{t("payment method", { capfirst: true })}</Table.Th>
|
||||
<Table.Th>{t("total price", { capfirst: true })}</Table.Th>
|
||||
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Tabs } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router";
|
||||
import { useAuth } from "@/services/auth/AuthProvider";
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const {loggedUser} = useAuth();
|
||||
return (
|
||||
<Tabs
|
||||
w={{ base: "100%", md: "80%", lg: "60%" }}
|
||||
@@ -21,7 +22,11 @@ export default function Dashboard() {
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/forms" {...props}></Link>)} value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/shipments" {...props}></Link>)} value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/contracts" {...props}></Link>)} value="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/users" {...props}></Link>)} value="users">{t("users", { capfirst: true })}</Tabs.Tab>
|
||||
{
|
||||
loggedUser?.user?.roles && loggedUser?.user?.roles?.length > 5 ?
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/users" {...props}></Link>)} value="users">{t("users", { capfirst: true })}</Tabs.Tab> :
|
||||
null
|
||||
}
|
||||
</Tabs.List>
|
||||
<Outlet />
|
||||
</Tabs>
|
||||
|
||||
@@ -3,21 +3,20 @@ import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Blockquote,
|
||||
Group,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TableOfContents,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconDownload,
|
||||
IconEdit,
|
||||
IconInfoCircle,
|
||||
IconLink,
|
||||
IconPlus,
|
||||
IconTableExport,
|
||||
IconTestPipe,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -77,7 +76,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("productors") })}
|
||||
{t("button in top right of the page", { section: t("productors") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -127,7 +126,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("products") })}
|
||||
{t("button in top right of the page", { section: t("products") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -178,7 +177,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("forms") })}
|
||||
{t("button in top right of the page", { section: t("forms") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -225,7 +224,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("shipments") })}
|
||||
{t("button in top right of the page", { section: t("shipments") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -243,6 +242,41 @@ export function Help() {
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={3}>{t("export contracts", {capfirst: true})}</Title>
|
||||
<Stack>
|
||||
<Text>
|
||||
{t("to export contracts submissions before sending to the productor go to the contracts section", {capfirst: true})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/contracts"
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("shipments"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>{t("in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract", {capfirst: true})}</Text>
|
||||
<Text>
|
||||
{t("you can download all contracts for your form using the export all", {capfirst: true})}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconDownload/>
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top right of the page", { section: t("contracts") })}{" "}
|
||||
{t("in the same corner you can download a recap by clicking on the button", {capfirst: true})}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconTableExport/>
|
||||
</ActionIcon>{" "}
|
||||
|
||||
</Text>
|
||||
<Text>
|
||||
{t("once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page", {capfirst: true})}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Title order={3}>{t("glossary", { capfirst: true })}</Title>
|
||||
<Stack>
|
||||
<Title order={4} fw={700}>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import { Flex, Stack, Text } from "@mantine/core";
|
||||
import { useGetForms } from "@/services/api";
|
||||
import { FormCard } from "@/components/Forms/Card";
|
||||
import type { Form } from "@/services/resources/forms";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { useEffect } from "react";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
|
||||
export function Home() {
|
||||
const { data: allForms } = useGetForms();
|
||||
const { data: allForms } = useGetForms(new URLSearchParams("?current_season=true"));
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("sessionExpired")) {
|
||||
showNotification({
|
||||
title: t("session expired", {capfirst: true}),
|
||||
message: t("your session has expired please log in again", {capfirst: true}),
|
||||
color: "red",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
if (searchParams.get("userNotAllowed")) {
|
||||
showNotification({
|
||||
title: t("user not allowed", {capfirst: true}),
|
||||
message: t("your keycloak user has no roles, please contact your administrator", {capfirst: true}),
|
||||
color: "red",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<Flex gap="md" wrap="wrap" justify="center">
|
||||
{allForms && allForms?.length > 0 ? (
|
||||
allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
|
||||
) : (
|
||||
<Text mt="lg" size="lg">
|
||||
{t("there is no contract for now", { capfirst: true })}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Stack mt="lg">
|
||||
<Flex gap="md" wrap="wrap" justify="center">
|
||||
{allForms && allForms?.length > 0 ? (
|
||||
allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
|
||||
) : (
|
||||
<Text mt="lg" size="lg">
|
||||
{t("there is no contract for now", { capfirst: true })}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ export default function Productors() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: productors, isPending } = useGetProductors(searchParams);
|
||||
|
||||
const { data: allProductors } = useGetProductors();
|
||||
const isCreate = location.pathname === "/dashboard/productors/create";
|
||||
const isEdit = location.pathname.includes("/edit");
|
||||
|
||||
@@ -29,15 +31,13 @@ export default function Productors() {
|
||||
return null;
|
||||
}, [location, isEdit]);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
const { data: productors, isPending } = useGetProductors(searchParams);
|
||||
const { data: currentProductor } = useGetProductor(Number(editId), {
|
||||
enabled: !!editId,
|
||||
});
|
||||
const { data: allProductors } = useGetProductors();
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
const names = useMemo(() => {
|
||||
return allProductors
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function Products() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isCreate = location.pathname === "/dashboard/products/create";
|
||||
const isEdit = location.pathname.includes("/edit");
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const router = createBrowserRouter([
|
||||
{ path: "products/:id/edit", Component: Products },
|
||||
{ path: "contracts", Component: Contracts },
|
||||
{ path: "contracts/download", Component: Contracts },
|
||||
{ path: "contracts/export", Component: Contracts },
|
||||
{ path: "users", Component: Users },
|
||||
{ path: "users/create", Component: Users },
|
||||
{ path: "users/:id/edit", Component: Users },
|
||||
|
||||
@@ -38,7 +38,8 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
|
||||
if (res.status === 401) {
|
||||
const refresh = await refreshToken();
|
||||
if (refresh.status == 400 || refresh.status == 401) {
|
||||
window.location.href = `${Config.backend_uri}/auth/logout`;
|
||||
window.location.href = `/?sessionExpired=True`;
|
||||
|
||||
const error = new Error("Unauthorized");
|
||||
error.cause = 401
|
||||
throw error;
|
||||
@@ -49,6 +50,9 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
|
||||
});
|
||||
return newRes;
|
||||
}
|
||||
if (res.status == 403) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -693,7 +697,33 @@ export function useGetContractFile() {
|
||||
disposition && disposition?.includes("filename=")
|
||||
? disposition.split("filename=")[1].replace(/"/g, "")
|
||||
: `contract_${id}.pdf`;
|
||||
console.log(disposition);
|
||||
return { blob, filename };
|
||||
},
|
||||
onSuccess: ({ blob, filename }) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetRecap() {
|
||||
return useMutation({
|
||||
mutationFn: async (form_id: number) => {
|
||||
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/recap`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res);
|
||||
|
||||
if (!res.ok) throw new Error();
|
||||
const blob = await res.blob();
|
||||
const disposition = res.headers.get("Content-Disposition");
|
||||
const filename =
|
||||
disposition && disposition?.includes("filename=")
|
||||
? disposition.split("filename=")[1].replace(/"/g, "")
|
||||
: `contract_recap_${form_id}.odt`;
|
||||
return { blob, filename };
|
||||
},
|
||||
onSuccess: ({ blob, filename }) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Contract = {
|
||||
phone: string;
|
||||
payment_method: string;
|
||||
cheque_quantity: number;
|
||||
total_price: number;
|
||||
};
|
||||
|
||||
export type ContractCreate = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export type User = {
|
||||
name: string;
|
||||
email: string;
|
||||
products: Product[];
|
||||
roles: Role[];
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export type UserInputs = {
|
||||
|
||||
Reference in New Issue
Block a user