add logout logic and wip recap

This commit is contained in:
Julien Aldon
2026-02-18 18:08:30 +01:00
parent aca24ca560
commit acbaadff67
29 changed files with 363 additions and 100 deletions

View File

@@ -6,9 +6,6 @@
- Extract recap - Extract recap
- Extract all contracts - Extract all contracts
- store total price - store total price
- store pdf file
- fix price display (reccurent in contract template)
## Wording ## Wording
@@ -42,5 +39,3 @@
## Update contract after register ## Update contract after register
## Default filter ## Default filter
## token expired refresh token

View File

@@ -29,6 +29,7 @@ dependencies = [
"cryptography", "cryptography",
"requests", "requests",
"weasyprint", "weasyprint",
"odfdo"
] ]
[project.urls] [project.urls]

View File

@@ -13,7 +13,7 @@ from src.models import UserCreate, User, UserPublic
import secrets import secrets
import requests import requests
from urllib.parse import urlencode
import src.messages as messages import src.messages as messages
router = APIRouter(prefix='/auth') router = APIRouter(prefix='/auth')
@@ -23,24 +23,13 @@ security = HTTPBearer()
@router.get('/logout') @router.get('/logout')
def logout( def logout(
id_token: Annotated[str | None, Cookie()] = None,
refresh_token: Annotated[str | None, Cookie()] = None, refresh_token: Annotated[str | None, Cookie()] = None,
): ):
if refresh_token: params = {
print("invalidate tokens") 'client_id': settings.keycloak_client_id,
requests.post(LOGOUT_URL, data={ 'post_logout_redirect_uri': settings.origins,
"client_id": settings.keycloak_client_id, }
"client_secret": settings.keycloak_client_secret, response = RedirectResponse(f'{LOGOUT_URL}?{urlencode(params)}')
"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")
response.delete_cookie( response.delete_cookie(
key='access_token', key='access_token',
path='/', path='/',
@@ -59,6 +48,12 @@ def logout(
secure=not settings.debug, secure=not settings.debug,
samesite='lax', 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 return response
@@ -107,9 +102,9 @@ def callback(code: str, session: Session = Depends(get_session)):
'refresh_token': token_data['refresh_token'], 'refresh_token': token_data['refresh_token'],
} }
res = requests.post(LOGOUT_URL, data=data) res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins) resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp return resp
resource_access.get(settings.keycloak_client_id) roles = resource_access.get(settings.keycloak_client_id)
if not roles: if not roles:
data = { data = {
'client_id': settings.keycloak_client_id, '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'], 'refresh_token': token_data['refresh_token'],
} }
res = requests.post(LOGOUT_URL, data=data) res = requests.post(LOGOUT_URL, data=data)
resp = RedirectResponse(settings.origins) resp = RedirectResponse(f'{settings.origins}?userNotAllowed=true')
return resp return resp
user_create = UserCreate( user_create = UserCreate(

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from src.database import get_session from src.database import get_session
from sqlmodel import 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 from src.auth.auth import get_current_user
import src.models as models import src.models as models
import src.messages as messages import src.messages as messages
@@ -79,7 +79,8 @@ async def create_contract(
occasionals = create_occasional_dict(occasional_contract_products) 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))) 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)) 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)) cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
# TODO: send contract to referer # TODO: send contract to referer
@@ -94,7 +95,7 @@ async def create_contract(
) )
pdf_file = io.BytesIO(pdf_bytes) pdf_file = io.BytesIO(pdf_bytes)
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}' 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: except Exception as e:
print(e) print(e)
raise HTTPException(status_code=400, detail=messages.pdferror) raise HTTPException(status_code=400, detail=messages.pdferror)
@@ -112,7 +113,7 @@ def get_contracts(
session: Session = Depends(get_session), session: Session = Depends(get_session),
user: models.User = Depends(get_current_user) user: models.User = Depends(get_current_user)
): ):
return service.get_all(session, forms) return service.get_all(session, user, forms)
@router.get('/{id}/file') @router.get('/{id}/file')
def get_contract_file( def get_contract_file(
@@ -120,6 +121,8 @@ def get_contract_file(
session: Session = Depends(get_session), session: Session = Depends(get_session),
user: models.User = Depends(get_current_user) 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) contract = service.get_one(session, id)
if contract is None: if contract is None:
raise HTTPException(status_code=404, detail=messages.notfound) raise HTTPException(status_code=404, detail=messages.notfound)
@@ -138,8 +141,10 @@ def get_contract_files(
session: Session = Depends(get_session), session: Session = Depends(get_session),
user: models.User = Depends(get_current_user) 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) 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() zipped_contracts = io.BytesIO()
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file: with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
for contract in contracts: 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) @router.get('/{id}', response_model=models.ContractPublic)
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)): 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) result = service.get_one(session, id)
if result is None: if result is None:
raise HTTPException(status_code=404, detail=messages.notfound) 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) @router.delete('/{id}', response_model=models.ContractPublic)
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)): 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) result = service.delete_one(session, id)
if result is None: if result is None:
raise HTTPException(status_code=404, detail=messages.notfound) raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -3,6 +3,7 @@ import jinja2
import src.models as models import src.models as models
import html import html
from weasyprint import HTML from weasyprint import HTML
import io
def generate_html_contract( def generate_html_contract(
contract: models.Contract, contract: models.Contract,
@@ -58,3 +59,24 @@ def generate_html_contract(
string=output_text, string=output_text,
base_url=template_dir 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()

View File

@@ -3,14 +3,19 @@ import src.models as models
def get_all( def get_all(
session: Session, session: Session,
user: models.User,
forms: list[str] = [], forms: list[str] = [],
form_id: int | None = None, form_id: int | None = None,
) -> list[models.ContractPublic]: ) -> list[models.ContractPublic]:
statement = select(models.Contract) statement = select(models.Contract)\
if form_id: .join(models.Form, models.Contract.form_id == models.Form.id)\
statement = statement.join(models.Form).where(models.Form.id == 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: 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() return session.exec(statement.order_by(models.Contract.id)).all()
def get_one(session: Session, contract_id: int) -> models.ContractPublic: 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) session.refresh(new_contract)
return 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) statement = select(models.Contract).where(models.Contract.id == id)
result = session.exec(statement) result = session.exec(statement)
contract = result.first() contract = result.first()
contract.total_price = price
contract.file = file contract.file = file
session.add(contract) session.add(contract)
session.commit() session.commit()
@@ -77,3 +82,12 @@ def delete_one(session: Session, id: int) -> models.ContractPublic:
session.delete(contract) session.delete(contract)
session.commit() session.commit()
return result 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

View File

@@ -12,9 +12,10 @@ router = APIRouter(prefix='/forms')
async def get_forms( async def get_forms(
seasons: list[str] = Query([]), seasons: list[str] = Query([]),
productors: list[str] = Query([]), productors: list[str] = Query([]),
current_season: bool = False,
session: Session = Depends(get_session) 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) @router.get('/{id}', response_model=models.FormPublic)
async def get_form(id: int, session: Session = Depends(get_session)): async def get_form(id: int, session: Session = Depends(get_session)):

View File

@@ -1,16 +1,35 @@
from sqlmodel import Session, select from sqlmodel import Session, select
import src.models as models import src.models as models
from sqlalchemy import func
def get_all( def get_all(
session: Session, session: Session,
seasons: list[str], seasons: list[str],
productors: list[str] productors: list[str],
current_season: bool,
) -> list[models.FormPublic]: ) -> list[models.FormPublic]:
statement = select(models.Form) statement = select(models.Form)
if len(seasons) > 0: if len(seasons) > 0:
statement = statement.where(models.Form.season.in_(seasons)) statement = statement.where(models.Form.season.in_(seasons))
if len(productors) > 0: if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors)) 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() return session.exec(statement.order_by(models.Form.name)).all()
def get_one(session: Session, form_id: int) -> models.FormPublic: 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.delete(form)
session.commit() session.commit()
return result 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

View File

@@ -7,3 +7,4 @@ usernotfound = "User not found"
userloggedout = "User logged out" userloggedout = "User logged out"
failtogettoken = "Failed to get token" failtogettoken = "Failed to get token"
unauthorized = "Unauthorized" unauthorized = "Unauthorized"
notallowed = "Not Allowed"

View File

@@ -222,6 +222,7 @@ class Contract(ContractBase, table=True):
cascade_delete=True cascade_delete=True
) )
file: bytes = Field(sa_column=Column(LargeBinary)) file: bytes = Field(sa_column=Column(LargeBinary))
total_price: float | None
class ContractCreate(ContractBase): class ContractCreate(ContractBase):
products: list["ContractProductCreate"] = [] products: list["ContractProductCreate"] = []
@@ -235,6 +236,7 @@ class ContractPublic(ContractBase):
id: int id: int
products: list["ContractProduct"] = [] products: list["ContractProduct"] = []
form: Form form: Form
total_price: float | None
# file: bytes # file: bytes
class ContractProductBase(SQLModel): class ContractProductBase(SQLModel):

View File

@@ -16,7 +16,7 @@ def get_all(
def get_one(session: Session, user_id: int) -> models.UserPublic: def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id) 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)) statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
existing = session.exec(statement).all() existing = session.exec(statement).all()
existing_roles = {role.name for role in existing} 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) statement = select(models.User).where(models.User.email == user_create.email)
user = session.exec(statement).first() user = session.exec(statement).first()
if user: 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 return user
user = create_one(session, user_create) user = create_one(session, user_create)
return user return user
@@ -46,7 +49,6 @@ def get_roles(session: Session):
return session.exec(statement.order_by(models.ContractType.name)).all() return session.exec(statement.order_by(models.ContractType.name)).all()
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic: def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
print("USER CREATE", user)
new_user = models.User( new_user = models.User(
name=user.name, name=user.name,
email=user.email email=user.email
@@ -60,15 +62,20 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
session.refresh(new_user) session.refresh(new_user)
return 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) statement = select(models.User).where(models.User.id == id)
result = session.exec(statement) result = session.exec(statement)
new_user = result.first() new_user = result.first()
if not new_user: if not new_user:
return None 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(): for key, value in user_updates.items():
setattr(new_user, key, value) setattr(new_user, key, value)
roles = get_or_create_roles(session, user.role_names)
new_user.roles = roles
session.add(new_user) session.add(new_user)
session.commit() session.commit()
session.refresh(new_user) session.refresh(new_user)

View File

@@ -29,7 +29,7 @@
"edit form": "edit contract form", "edit form": "edit contract form",
"form name": "contract form name", "form name": "contract form name",
"contract season": "contract season", "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", "start date": "start date",
"end date": "end date", "end date": "end date",
"nothing found": "nothing to display", "nothing found": "nothing to display",
@@ -58,6 +58,7 @@
"quantity unit": "quantity unit", "quantity unit": "quantity unit",
"unit": "sales unit", "unit": "sales unit",
"price": "price", "price": "price",
"total price": "total price",
"create product": "create product", "create product": "create product",
"informations": "information", "informations": "information",
"remove product": "remove product", "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).", "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).", "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.", "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", "contracts": "contracts",
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least", "minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
"there is": "there is", "there is": "there is",
@@ -130,7 +138,7 @@
"to add a use the": "to add {{section}} use the button", "to add a use the": "to add {{section}} use the button",
"to edit a use the": "to edit {{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", "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 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).", "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", "glossary": "glossary",
@@ -188,6 +196,10 @@
"enter payment method": "select your payment method", "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.", "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", "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).", "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.", "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." "all theses informations are for contract generation": "all this information is required for contract generation."

View File

@@ -29,7 +29,7 @@
"edit form": "modifier le formulaire de contrat", "edit form": "modifier le formulaire de contrat",
"form name": "nom du formulaire de contrat", "form name": "nom du formulaire de contrat",
"contract season": "saison du 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", "start date": "date de début",
"end date": "date de fin", "end date": "date de fin",
"nothing found": "rien à afficher", "nothing found": "rien à afficher",
@@ -58,6 +58,7 @@
"quantity unit": "unité de quantité", "quantity unit": "unité de quantité",
"unit": "unité de vente", "unit": "unité de vente",
"price": "prix", "price": "prix",
"total price": "prix total",
"create product": "créer le produit", "create product": "créer le produit",
"informations": "informations", "informations": "informations",
"remove product": "supprimer le produit", "remove product": "supprimer le produit",
@@ -129,7 +130,7 @@
"to add a use the": "pour ajouter {{section}} utilisez le bouton", "to add a use the": "pour ajouter {{section}} utilisez le bouton",
"to edit a use the": "pour éditer {{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", "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 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).", "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", "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\".", "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\".", "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.", "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", "create shipments for your contract form": "créez les livraisons pour votre contrat",
"creation order": "ordre de création", "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.", "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", "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).", "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", "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.", "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", "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).", "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.", "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." "all theses informations are for contract generation": "ces informations sont nécessaires pour la génération de contrat."

View File

@@ -28,6 +28,14 @@ export default function ContractRow({ contract }: ContractRowProps) {
<Table.Td> <Table.Td>
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method} {contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
</Table.Td> </Table.Td>
<Table.Td>
{
`${Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(contract.total_price)}`
}
</Table.Td>
<Table.Td> <Table.Td>
<Tooltip label={t("download contract", { capfirst: true })}> <Tooltip label={t("download contract", { capfirst: true })}>
<ActionIcon <ActionIcon

View File

@@ -41,7 +41,6 @@ export function Navbar() {
href={`${Config.backend_uri}/auth/logout`} href={`${Config.backend_uri}/auth/logout`}
className={"navLink"} className={"navLink"}
aria-label={t("logout", { capfirst: true })} aria-label={t("logout", { capfirst: true })}
> >
{t("logout", { capfirst: true })} {t("logout", { capfirst: true })}
</a> </a>

View File

@@ -1,6 +1,6 @@
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { ContractInputs } from "@/services/resources/contracts"; 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 type { UseFormReturnType } from "@mantine/form";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -56,12 +56,17 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
{...inputForm.getInputProps(`cheque_quantity`)} {...inputForm.getInputProps(`cheque_quantity`)}
/> />
<Group grow> <Group grow>
{inputForm.values.cheques.map((_cheque, index) => ( {inputForm.values.cheques.map((cheque, index) => (
<Stack key={`${index}`}> <Stack key={`${index}`}>
<TextInput <TextInput
label={t("cheque id", { capfirst: true })} label={t("cheque id", { capfirst: true })}
placeholder={t("cheque id", { capfirst: true })} placeholder={t("cheque id", { capfirst: true })}
{...inputForm.getInputProps(`cheques.${index}.name`)} {...inputForm.getInputProps(`cheques.${index}.name`)}
error={
cheque.name == "" ?
<Text size="sm" c="red">{inputForm?.errors.cheques}</Text> :
null
}
/> />
<NumberInput <NumberInput
readOnly readOnly

View File

@@ -21,7 +21,6 @@ export default function ProductorsFilter({
const defaultTypes = useMemo(() => { const defaultTypes = useMemo(() => {
return filters.getAll("types"); return filters.getAll("types");
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect

View File

@@ -46,6 +46,12 @@ export function ProductorModal({
!value ? `${t("address", { capfirst: true })} ${t("is required")}` : null, !value ? `${t("address", { capfirst: true })} ${t("is required")}` : null,
type: (value) => type: (value) =>
!value ? `${t("type", { capfirst: true })} ${t("is required")}` : null, !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 clearable
searchable searchable
value={form.values.payment_methods.map((p) => p.name)} value={form.values.payment_methods.map((p) => p.name)}
error={form.errors.payment_methods}
onChange={(names) => { onChange={(names) => {
form.setFieldValue( form.setFieldValue(
"payment_methods", "payment_methods",

View File

@@ -35,7 +35,7 @@ export function Contract() {
email: "", email: "",
phone: "", phone: "",
payment_method: "", payment_method: "",
cheque_quantity: 1, cheque_quantity: 0,
cheques: [], cheques: [],
products: {}, products: {},
}, },
@@ -50,6 +50,8 @@ export function Contract() {
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null, !value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
payment_method: (value) => payment_method: (value) =>
!value ? `${t("a payment method", { capfirst: true })} ${t("is required")}` : null, !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,
}, },
}); });

View File

@@ -1,7 +1,7 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useGetAllContractFile, useGetContracts } from "@/services/api"; import { useGetAllContractFile, useGetContracts, useGetRecap } from "@/services/api";
import { IconDownload } from "@tabler/icons-react"; import { IconDownload, IconTableExport } from "@tabler/icons-react";
import ContractRow from "@/components/Contracts/Row"; import ContractRow from "@/components/Contracts/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ContractModal } from "@/components/Contracts/Modal"; import { ContractModal } from "@/components/Contracts/Modal";
@@ -14,7 +14,9 @@ export default function Contracts() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const getAllContractFilesMutation = useGetAllContractFile(); const getAllContractFilesMutation = useGetAllContractFile();
const getRecapMutation = useGetRecap();
const isdownload = location.pathname.includes("/download"); const isdownload = location.pathname.includes("/download");
const isrecap = location.pathname.includes("/export");
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`); navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
@@ -52,6 +54,13 @@ export default function Contracts() {
[getAllContractFilesMutation], [getAllContractFilesMutation],
); );
const handleDownloadRecap = useCallback(
async (id: number) => {
await getRecapMutation.mutateAsync(id);
},
[getAllContractFilesMutation],
)
if (!contracts || isPending) if (!contracts || isPending)
return ( return (
<Group align="center" justify="center" h="80vh" w="100%"> <Group align="center" justify="center" h="80vh" w="100%">
@@ -63,6 +72,7 @@ export default function Contracts() {
<Stack> <Stack>
<Group justify="space-between"> <Group justify="space-between">
<Title order={2}>{t("all contracts", { capfirst: true })}</Title> <Title order={2}>{t("all contracts", { capfirst: true })}</Title>
<Group>
<Tooltip label={t("download contracts", { capfirst: true })}> <Tooltip label={t("download contracts", { capfirst: true })}>
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
@@ -75,11 +85,32 @@ export default function Contracts() {
<IconDownload /> <IconDownload />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip
label={t("download recap", { capfirst: true })}
>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/export${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconTableExport />
</ActionIcon>
</Tooltip>
</Group>
<ContractModal <ContractModal
opened={isdownload} opened={isdownload}
onClose={closeModal} onClose={closeModal}
handleSubmit={handleDownloadContracts} handleSubmit={handleDownloadContracts}
/> />
<ContractModal
opened={isrecap}
onClose={closeModal}
handleSubmit={handleDownloadRecap}
/>
</Group> </Group>
<ContractsFilters <ContractsFilters
forms={forms || []} forms={forms || []}
@@ -94,6 +125,7 @@ export default function Contracts() {
<Table.Th>{t("name", { capfirst: true })}</Table.Th> <Table.Th>{t("name", { capfirst: true })}</Table.Th>
<Table.Th>{t("email", { capfirst: true })}</Table.Th> <Table.Th>{t("email", { capfirst: true })}</Table.Th>
<Table.Th>{t("payment method", { 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.Th>{t("actions", { capfirst: true })}</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>

View File

@@ -1,11 +1,12 @@
import { Tabs } from "@mantine/core"; import { Tabs } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { Link, Outlet, useLocation, useNavigate } from "react-router"; import { Link, Outlet, useLocation, useNavigate } from "react-router";
import { useAuth } from "@/services/auth/AuthProvider";
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const {loggedUser} = useAuth();
return ( return (
<Tabs <Tabs
w={{ base: "100%", md: "80%", lg: "60%" }} 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/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/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/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> </Tabs.List>
<Outlet /> <Outlet />
</Tabs> </Tabs>

View File

@@ -3,21 +3,20 @@ import {
Accordion, Accordion,
ActionIcon, ActionIcon,
Blockquote, Blockquote,
Group,
NumberInput, NumberInput,
Paper,
Select, Select,
Stack, Stack,
TableOfContents, TableOfContents,
Text, Text,
TextInput,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconDownload,
IconEdit, IconEdit,
IconInfoCircle, IconInfoCircle,
IconLink, IconLink,
IconPlus, IconPlus,
IconTableExport,
IconTestPipe, IconTestPipe,
IconX, IconX,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@@ -77,7 +76,7 @@ export function Help() {
<ActionIcon size="sm"> <ActionIcon size="sm">
<IconPlus /> <IconPlus />
</ActionIcon>{" "} </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>
<Text> <Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "} {t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
@@ -127,7 +126,7 @@ export function Help() {
<ActionIcon size="sm"> <ActionIcon size="sm">
<IconPlus /> <IconPlus />
</ActionIcon>{" "} </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>
<Text> <Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "} {t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
@@ -178,7 +177,7 @@ export function Help() {
<ActionIcon size="sm"> <ActionIcon size="sm">
<IconPlus /> <IconPlus />
</ActionIcon>{" "} </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>
<Text> <Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "} {t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
@@ -225,7 +224,7 @@ export function Help() {
<ActionIcon size="sm"> <ActionIcon size="sm">
<IconPlus /> <IconPlus />
</ActionIcon>{" "} </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>
<Text> <Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "} {t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
@@ -243,6 +242,41 @@ export function Help() {
</Text> </Text>
</Blockquote> </Blockquote>
</Stack> </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> <Title order={3}>{t("glossary", { capfirst: true })}</Title>
<Stack> <Stack>
<Title order={4} fw={700}> <Title order={4} fw={700}>

View File

@@ -1,13 +1,37 @@
import { Flex, Text } from "@mantine/core"; import { Flex, Stack, Text } from "@mantine/core";
import { useGetForms } from "@/services/api"; import { useGetForms } from "@/services/api";
import { FormCard } from "@/components/Forms/Card"; import { FormCard } from "@/components/Forms/Card";
import type { Form } from "@/services/resources/forms"; import type { Form } from "@/services/resources/forms";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useSearchParams } from "react-router";
import { useEffect } from "react";
import { showNotification } from "@mantine/notifications";
export function Home() { 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 ( return (
<Stack mt="lg">
<Flex gap="md" wrap="wrap" justify="center"> <Flex gap="md" wrap="wrap" justify="center">
{allForms && allForms?.length > 0 ? ( {allForms && allForms?.length > 0 ? (
allForms.map((form: Form) => <FormCard form={form} key={form.id} />) allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
@@ -17,5 +41,6 @@ export function Home() {
</Text> </Text>
)} )}
</Flex> </Flex>
</Stack>
); );
} }

View File

@@ -18,7 +18,9 @@ export default function Productors() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: productors, isPending } = useGetProductors(searchParams);
const { data: allProductors } = useGetProductors();
const isCreate = location.pathname === "/dashboard/productors/create"; const isCreate = location.pathname === "/dashboard/productors/create";
const isEdit = location.pathname.includes("/edit"); const isEdit = location.pathname.includes("/edit");
@@ -29,15 +31,13 @@ export default function Productors() {
return null; return null;
}, [location, isEdit]); }, [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), { const { data: currentProductor } = useGetProductor(Number(editId), {
enabled: !!editId, enabled: !!editId,
}); });
const { data: allProductors } = useGetProductors();
const closeModal = useCallback(() => {
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const names = useMemo(() => { const names = useMemo(() => {
return allProductors return allProductors

View File

@@ -17,7 +17,6 @@ export default function Products() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/products/create"; const isCreate = location.pathname === "/dashboard/products/create";
const isEdit = location.pathname.includes("/edit"); const isEdit = location.pathname.includes("/edit");

View File

@@ -39,6 +39,7 @@ export const router = createBrowserRouter([
{ path: "products/:id/edit", Component: Products }, { path: "products/:id/edit", Component: Products },
{ path: "contracts", Component: Contracts }, { path: "contracts", Component: Contracts },
{ path: "contracts/download", Component: Contracts }, { path: "contracts/download", Component: Contracts },
{ path: "contracts/export", Component: Contracts },
{ path: "users", Component: Users }, { path: "users", Component: Users },
{ path: "users/create", Component: Users }, { path: "users/create", Component: Users },
{ path: "users/:id/edit", Component: Users }, { path: "users/:id/edit", Component: Users },

View File

@@ -38,7 +38,8 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
if (res.status === 401) { if (res.status === 401) {
const refresh = await refreshToken(); const refresh = await refreshToken();
if (refresh.status == 400 || refresh.status == 401) { 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"); const error = new Error("Unauthorized");
error.cause = 401 error.cause = 401
throw error; throw error;
@@ -49,6 +50,9 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
}); });
return newRes; return newRes;
} }
if (res.status == 403) {
throw new Error(res.statusText);
}
return res; return res;
} }
@@ -693,7 +697,33 @@ export function useGetContractFile() {
disposition && disposition?.includes("filename=") disposition && disposition?.includes("filename=")
? disposition.split("filename=")[1].replace(/"/g, "") ? disposition.split("filename=")[1].replace(/"/g, "")
: `contract_${id}.pdf`; : `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 }; return { blob, filename };
}, },
onSuccess: ({ blob, filename }) => { onSuccess: ({ blob, filename }) => {

View File

@@ -14,6 +14,7 @@ export type Contract = {
phone: string; phone: string;
payment_method: string; payment_method: string;
cheque_quantity: number; cheque_quantity: number;
total_price: number;
}; };
export type ContractCreate = { export type ContractCreate = {

View File

@@ -15,7 +15,7 @@ export type User = {
name: string; name: string;
email: string; email: string;
products: Product[]; products: Product[];
roles: Role[]; roles: string[];
}; };
export type UserInputs = { export type UserInputs = {