Add authentification
This commit is contained in:
14
README.md
14
README.md
@@ -8,19 +8,21 @@
|
||||
- store total price
|
||||
- store pdf file
|
||||
|
||||
- fix price display (reccurent in contract template)
|
||||
|
||||
## Wording
|
||||
|
||||
- all translations
|
||||
|
||||
## Draft / Publish form
|
||||
|
||||
- By default form is in draft mode
|
||||
- Validate a form (button)
|
||||
- check if productor
|
||||
- check if shipments
|
||||
- check products
|
||||
- check if productor
|
||||
- check if shipments
|
||||
- check products
|
||||
- Publish
|
||||
|
||||
|
||||
## Footer
|
||||
|
||||
### Legal
|
||||
@@ -38,3 +40,7 @@
|
||||
## Only show current season (if multiple form, only show the one with latest start date)
|
||||
|
||||
## Update contract after register
|
||||
|
||||
## Default filter
|
||||
|
||||
## token expired refresh token
|
||||
|
||||
@@ -1,91 +1,140 @@
|
||||
from fastapi import APIRouter, Security, HTTPException, Depends
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi import APIRouter, Security, HTTPException, Depends, Request
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlmodel import Session
|
||||
from sqlmodel import Session, select
|
||||
import jwt
|
||||
from jwt import PyJWKClient
|
||||
|
||||
from src.settings import AUTH_URL, TOKEN_URL, JWKS_URL, ISSUER, settings
|
||||
import src.users.service as service
|
||||
from src.database import get_session
|
||||
from src.models import UserCreate
|
||||
from src.models import UserCreate, User, UserPublic
|
||||
|
||||
import secrets
|
||||
import jwt
|
||||
from jwt import PyJWKClient
|
||||
import requests
|
||||
|
||||
from src.messages import tokenExpired, invalidToken
|
||||
import src.messages as messages
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
router = APIRouter(prefix='/auth')
|
||||
|
||||
jwk_client = PyJWKClient(JWKS_URL)
|
||||
security = HTTPBearer()
|
||||
|
||||
@router.post('/logout')
|
||||
def logout(response: Response):
|
||||
response.delete_cookie('access_token')
|
||||
response.delete_cookie('refresh_token')
|
||||
return {'detail': messages.userloggedout}
|
||||
|
||||
|
||||
@router.get('/login')
|
||||
def login():
|
||||
state = secrets.token_urlsafe(16)
|
||||
params = {
|
||||
"client_id": settings.keycloak_client_id,
|
||||
"response_type": "code",
|
||||
"scope": "openid",
|
||||
"redirect_uri": settings.keycloak_redirect_uri,
|
||||
"state": state,
|
||||
'client_id': settings.keycloak_client_id,
|
||||
'response_type': 'code',
|
||||
'scope': 'openid',
|
||||
'redirect_uri': settings.keycloak_redirect_uri,
|
||||
'state': state,
|
||||
}
|
||||
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
|
||||
return RedirectResponse(request_url)
|
||||
|
||||
@router.get("/callback")
|
||||
@router.get('/callback')
|
||||
def callback(code: str, session: Session = Depends(get_session)):
|
||||
data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": settings.keycloak_redirect_uri,
|
||||
"client_id": settings.keycloak_client_id,
|
||||
"client_secret": settings.keycloak_client_secret,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': settings.keycloak_redirect_uri,
|
||||
'client_id': settings.keycloak_client_id,
|
||||
'client_secret': settings.keycloak_client_secret,
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
response = requests.post(TOKEN_URL, data=data, headers=headers)
|
||||
if response.status_code != 200:
|
||||
return JSONResponse(
|
||||
{"error": "Failed to get token"},
|
||||
status_code=400
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=messages.failtogettoken
|
||||
)
|
||||
|
||||
token_data = response.json()
|
||||
|
||||
id_token = token_data["id_token"]
|
||||
decoded_token = jwt.decode(id_token, options={"verify_signature": False})
|
||||
id_token = token_data['id_token']
|
||||
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
|
||||
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
|
||||
roles = decoded_access_token['resource_access'][settings.keycloak_client_id]
|
||||
user_create = UserCreate(
|
||||
email=decoded_token.get("email"),
|
||||
name=decoded_token.get("preferred_username")
|
||||
email=decoded_token.get('email'),
|
||||
name=decoded_token.get('preferred_username'),
|
||||
role_names=roles['roles']
|
||||
)
|
||||
user = service.get_or_create_user(session, user_create)
|
||||
return {
|
||||
"access_token": token_data["access_token"],
|
||||
"id_token": token_data["id_token"],
|
||||
"refresh_token": token_data["refresh_token"],
|
||||
}
|
||||
service.get_or_create_user(session, user_create)
|
||||
response = RedirectResponse(settings.origins)
|
||||
response.set_cookie(
|
||||
key='access_token',
|
||||
value=token_data['access_token'],
|
||||
httponly=True,
|
||||
secure=True if settings.debug == False else True,
|
||||
samesite='strict',
|
||||
max_age=settings.max_age
|
||||
)
|
||||
response.set_cookie(
|
||||
key='refresh_token',
|
||||
value=token_data['refresh_token'] or '',
|
||||
httponly=True,
|
||||
secure=True if settings.debug == False else True,
|
||||
samesite='strict',
|
||||
max_age=30 * 24 * settings.max_age
|
||||
)
|
||||
return response
|
||||
|
||||
def verify_token(token: str):
|
||||
try:
|
||||
signing_key = jwk_client.get_signing_key_from_jwt(token)
|
||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
||||
decoded = jwt.decode(token, options={'verify_signature': False})
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
signing_key.key,
|
||||
algorithms=["RS256"],
|
||||
algorithms=['RS256'],
|
||||
audience=settings.keycloak_client_id,
|
||||
issuer=ISSUER,
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail=tokenExpired)
|
||||
raise HTTPException(status_code=401, detail=messages.tokenexipired)
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=401, detail=invalidToken)
|
||||
raise HTTPException(status_code=401, detail=messages.invalidtoken)
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security)
|
||||
):
|
||||
return verify_token(credentials.credentials)
|
||||
def get_current_user(request: Request, session: Session = Depends(get_session)):
|
||||
access_token = request.cookies.get("access_token")
|
||||
if not access_token:
|
||||
raise HTTPException(status_code=401, detail=messages.notauthenticated)
|
||||
payload = verify_token(access_token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="aze")
|
||||
email = payload.get('email')
|
||||
|
||||
if not email:
|
||||
raise HTTPException(status_code=401, detail=messages.notauthenticated)
|
||||
|
||||
user = session.exec(select(User).where(User.email == email)).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail=messages.usernotfound)
|
||||
return user
|
||||
|
||||
@router.get('/user/me')
|
||||
def me(user: UserPublic = Depends(get_current_user)):
|
||||
if not user:
|
||||
return {"logged": False}
|
||||
return {
|
||||
"logged": True,
|
||||
"user": {
|
||||
"name": user.name,
|
||||
"email": user.email,
|
||||
"id": user.id,
|
||||
"roles": [role.name for role in user.roles]
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ 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.auth.auth import get_current_user
|
||||
import src.models as models
|
||||
from src.messages import PDFerrorOccured
|
||||
import src.messages as messages
|
||||
import src.contracts.service as service
|
||||
|
||||
import src.forms.service as form_service
|
||||
import io
|
||||
|
||||
import zipfile
|
||||
router = APIRouter(prefix='/contracts')
|
||||
|
||||
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
|
||||
@@ -71,7 +72,8 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
|
||||
@router.post('/')
|
||||
async def create_contract(
|
||||
contract: models.ContractCreate,
|
||||
session: Session = Depends(get_session)
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
new_contract = service.create_one(session, contract)
|
||||
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))
|
||||
@@ -93,50 +95,77 @@ 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, id, pdf_bytes)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail=PDFerrorOccured)
|
||||
service.add_contract_file(session, new_contract.id, pdf_bytes)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=400, detail=messages.pdferror)
|
||||
return StreamingResponse(
|
||||
pdf_file,
|
||||
media_type='application/pdf',
|
||||
headers={
|
||||
'Content-Disposition': f'attachement; filename=contract_{contract_id}.pdf'
|
||||
'Content-Disposition': f'attachment; filename=contract_{contract_id}.pdf'
|
||||
}
|
||||
)
|
||||
|
||||
@router.get('/', response_model=list[models.ContractPublic])
|
||||
def get_contracts(
|
||||
forms: list[str] = Query([]),
|
||||
session: Session = Depends(get_session)
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
return service.get_all(session, forms)
|
||||
|
||||
@router.get('/{id}/file')
|
||||
def get_contract_file(
|
||||
id: int,
|
||||
session: Session = Depends(get_session)
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
contract = service.get_one(session, id)
|
||||
print(contract.file)
|
||||
if contract is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}'
|
||||
return StreamingResponse(
|
||||
contract.file,
|
||||
io.BytesIO(contract.file),
|
||||
media_type='application/pdf',
|
||||
headers={
|
||||
'Content-Disposition': f'attachement; filename=contract_{contract.id}.pdf'
|
||||
'Content-Disposition': f'attachment; filename={filename}.pdf'
|
||||
}
|
||||
)
|
||||
|
||||
@router.get('/{form_id}/files')
|
||||
def get_contract_files(
|
||||
form_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user)
|
||||
):
|
||||
form = form_service.get_one(session, form_id=form_id)
|
||||
contracts = service.get_all(session, [form.name])
|
||||
zipped_contracts = io.BytesIO()
|
||||
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
||||
for contract in contracts:
|
||||
contract_filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}.pdf'
|
||||
zip_file.writestr(contract_filename, contract.file)
|
||||
|
||||
filename = f'{form.name.replace(" ", "_")}_{form.season}'
|
||||
return StreamingResponse(
|
||||
io.BytesIO(zipped_contracts.getvalue()),
|
||||
media_type='application/zip',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename={filename}.zip'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get('/{id}', response_model=models.ContractPublic)
|
||||
def get_contract(id: int, session: Session = Depends(get_session)):
|
||||
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ContractPublic)
|
||||
def delete_contract(id: int, session: Session = Depends(get_session)):
|
||||
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -3,9 +3,12 @@ import src.models as models
|
||||
|
||||
def get_all(
|
||||
session: Session,
|
||||
forms: list[str]
|
||||
forms: list[str] = [],
|
||||
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)
|
||||
if len(forms) > 0:
|
||||
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
|
||||
return session.exec(statement.order_by(models.Contract.id)).all()
|
||||
|
||||
@@ -4,6 +4,7 @@ import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.forms.service as service
|
||||
from src.auth.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix='/forms')
|
||||
|
||||
@@ -23,18 +24,30 @@ async def get_form(id: int, session: Session = Depends(get_session)):
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.FormPublic)
|
||||
async def create_form(form: models.FormCreate, session: Session = Depends(get_session)):
|
||||
async def create_form(
|
||||
form: models.FormCreate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, form)
|
||||
|
||||
@router.put('/{id}', response_model=models.FormPublic)
|
||||
async def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)):
|
||||
async def update_form(
|
||||
id: int, form: models.FormUpdate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, form)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.FormPublic)
|
||||
async def delete_form(id: int, session: Session = Depends(get_session)):
|
||||
async def delete_form(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -22,7 +22,7 @@ app.add_middleware(
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=['x-nbpage']
|
||||
expose_headers=['x-nbpage', 'Content-Disposition']
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
notfound = "Resource was not found."
|
||||
PDFerrorOccured = "An error occured during PDF generation please contact administrator"
|
||||
tokenExpired = "Token expired"
|
||||
invalidToken = "Invalid token"
|
||||
pdferror = "An error occured during PDF generation please contact administrator"
|
||||
tokenexipired = "Token expired"
|
||||
invalidtoken = "Invalid token"
|
||||
notauthenticated = "Not authenticated"
|
||||
usernotfound = "User not found"
|
||||
userloggedout = "User logged out"
|
||||
failtogettoken = "Failed to get token"
|
||||
@@ -3,22 +3,35 @@ from enum import StrEnum
|
||||
from typing import Optional
|
||||
import datetime
|
||||
|
||||
class ContractType(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
|
||||
class UserContractTypeLink(SQLModel, table=True):
|
||||
user_id: int = Field(foreign_key="user.id", primary_key=True)
|
||||
contract_type_id: int = Field(foreign_key="contracttype.id", primary_key=True)
|
||||
|
||||
class UserBase(SQLModel):
|
||||
name: str
|
||||
email: str
|
||||
|
||||
class UserPublic(UserBase):
|
||||
id: int
|
||||
roles: list[ContractType]
|
||||
|
||||
class User(UserBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
roles: list[ContractType] = Relationship(
|
||||
link_model=UserContractTypeLink
|
||||
)
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
name: str | None
|
||||
email: str | None
|
||||
role_names: list[str] | None
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
role_names: list[str] | None
|
||||
|
||||
class PaymentMethodBase(SQLModel):
|
||||
name: str
|
||||
|
||||
@@ -4,6 +4,7 @@ import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.productors.service as service
|
||||
from src.auth.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix='/productors')
|
||||
|
||||
@@ -11,30 +12,47 @@ router = APIRouter(prefix='/productors')
|
||||
def get_productors(
|
||||
names: list[str] = Query([]),
|
||||
types: list[str] = Query([]),
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.get_all(session, names, types)
|
||||
|
||||
@router.get('/{id}', response_model=models.ProductorPublic)
|
||||
def get_productor(id: int, session: Session = Depends(get_session)):
|
||||
def get_productor(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.ProductorPublic)
|
||||
def create_productor(productor: models.ProductorCreate, session: Session = Depends(get_session)):
|
||||
def create_productor(
|
||||
productor: models.ProductorCreate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, productor)
|
||||
|
||||
@router.put('/{id}', response_model=models.ProductorPublic)
|
||||
def update_productor(id: int, productor: models.ProductorUpdate, session: Session = Depends(get_session)):
|
||||
def update_productor(
|
||||
id: int, productor: models.ProductorUpdate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, productor)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ProductorPublic)
|
||||
def delete_productor(id: int, session: Session = Depends(get_session)):
|
||||
def delete_productor(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -9,6 +9,7 @@ router = APIRouter(prefix='/products')
|
||||
#user=Depends(get_current_user)
|
||||
@router.get('/', response_model=list[models.ProductPublic], )
|
||||
def get_products(
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session),
|
||||
names: list[str] = Query([]),
|
||||
types: list[str] = Query([]),
|
||||
@@ -22,25 +23,41 @@ def get_products(
|
||||
)
|
||||
|
||||
@router.get('/{id}', response_model=models.ProductPublic)
|
||||
def get_product(id: int, session: Session = Depends(get_session)):
|
||||
def get_product(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.ProductPublic)
|
||||
def create_product(product: models.ProductCreate, session: Session = Depends(get_session)):
|
||||
def create_product(
|
||||
product: models.ProductCreate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, product)
|
||||
|
||||
@router.put('/{id}', response_model=models.ProductPublic)
|
||||
def update_product(id: int, product: models.ProductUpdate, session: Session = Depends(get_session)):
|
||||
def update_product(
|
||||
id: int, product: models.ProductUpdate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, product)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ProductPublic)
|
||||
def delete_product(id: int, session: Session = Depends(get_session)):
|
||||
def delete_product(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -13,6 +13,8 @@ class Settings(BaseSettings):
|
||||
keycloak_client_secret: str
|
||||
keycloak_redirect_uri: str
|
||||
vite_api_url: str
|
||||
max_age: int
|
||||
debug: bool
|
||||
|
||||
class Config:
|
||||
env_file = "../.env"
|
||||
|
||||
@@ -23,25 +23,41 @@ def get_shipments(
|
||||
)
|
||||
|
||||
@router.get('/{id}', response_model=models.ShipmentPublic)
|
||||
def get_shipment(id: int, session: Session = Depends(get_session)):
|
||||
def get_shipment(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.ShipmentPublic)
|
||||
def create_shipment(shipment: models.ShipmentCreate, session: Session = Depends(get_session)):
|
||||
def create_shipment(
|
||||
shipment: models.ShipmentCreate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, shipment)
|
||||
|
||||
@router.put('/{id}', response_model=models.ShipmentPublic)
|
||||
def update_shipment(id: int, shipment: models.ShipmentUpdate, session: Session = Depends(get_session)):
|
||||
def update_shipment(
|
||||
id: int, shipment: models.ShipmentUpdate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, shipment)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.ShipmentPublic)
|
||||
def delete_shipment(id: int, session: Session = Depends(get_session)):
|
||||
def delete_shipment(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -4,33 +4,53 @@ import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.templates.service as service
|
||||
from src.auth.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix='/templates')
|
||||
|
||||
@router.get('/', response_model=list[models.TemplatePublic])
|
||||
def get_templates(session: Session = Depends(get_session)):
|
||||
def get_templates(
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.get_all(session)
|
||||
|
||||
@router.get('/{id}', response_model=models.TemplatePublic)
|
||||
def get_template(id: int, session: Session = Depends(get_session)):
|
||||
def get_template(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.TemplatePublic)
|
||||
def create_template(template: models.TemplateCreate, session: Session = Depends(get_session)):
|
||||
def create_template(
|
||||
template: models.TemplateCreate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, template)
|
||||
|
||||
@router.put('/{id}', response_model=models.TemplatePublic)
|
||||
def update_template(id: int, template: models.TemplateUpdate, session: Session = Depends(get_session)):
|
||||
def update_template(
|
||||
id: int, template: models.TemplateUpdate,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, template)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.TemplatePublic)
|
||||
def delete_template(id: int, session: Session = Depends(get_session)):
|
||||
def delete_template(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -16,6 +16,23 @@ 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]:
|
||||
statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
|
||||
existing = session.exec(statement).all()
|
||||
existing_roles = {role.name for role in existing}
|
||||
missing_role = set(role_names) - existing_roles
|
||||
|
||||
new_roles = []
|
||||
for role_name in missing_role:
|
||||
role = models.ContractType(name=role_name)
|
||||
session.add(role)
|
||||
new_roles.append(role)
|
||||
|
||||
session.commit()
|
||||
for role in new_roles:
|
||||
session.refresh(role)
|
||||
return existing + new_roles
|
||||
|
||||
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()
|
||||
@@ -24,9 +41,20 @@ def get_or_create_user(session: Session, user_create: models.UserCreate):
|
||||
user = create_one(session, user_create)
|
||||
return user
|
||||
|
||||
def get_roles(session: Session):
|
||||
statement = select(models.ContractType)
|
||||
return session.exec(statement.order_by(models.ContractType.name)).all()
|
||||
|
||||
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
|
||||
user_create = user.model_dump(exclude_unset=True)
|
||||
new_user = models.User(**user_create)
|
||||
print("USER CREATE", user)
|
||||
new_user = models.User(
|
||||
name=user.name,
|
||||
email=user.email
|
||||
)
|
||||
|
||||
roles = get_or_create_roles(session, user.role_names)
|
||||
new_user.roles = roles
|
||||
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
|
||||
@@ -4,12 +4,14 @@ import src.models as models
|
||||
from src.database import get_session
|
||||
from sqlmodel import Session
|
||||
import src.users.service as service
|
||||
from src.auth.auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix='/users')
|
||||
|
||||
@router.get('/', response_model=list[models.UserPublic])
|
||||
def get_users(
|
||||
session: Session = Depends(get_session),
|
||||
user: models.User = Depends(get_current_user),
|
||||
names: list[str] = Query([]),
|
||||
emails: list[str] = Query([]),
|
||||
):
|
||||
@@ -19,26 +21,50 @@ def get_users(
|
||||
emails,
|
||||
)
|
||||
|
||||
@router.get('/roles', response_model=list[models.ContractType])
|
||||
def get_roles(
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.get_roles(session)
|
||||
|
||||
@router.get('/{id}', response_model=models.UserPublic)
|
||||
def get_users(id: int, session: Session = Depends(get_session)):
|
||||
def get_users(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.get_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.post('/', response_model=models.UserPublic)
|
||||
def create_user(user: models.UserCreate, session: Session = Depends(get_session)):
|
||||
def create_user(
|
||||
user: models.UserCreate,
|
||||
logged_user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
return service.create_one(session, user)
|
||||
|
||||
@router.put('/{id}', response_model=models.UserPublic)
|
||||
def update_user(id: int, user: models.UserUpdate, session: Session = Depends(get_session)):
|
||||
def update_user(
|
||||
id: int,
|
||||
user: models.UserUpdate,
|
||||
logged_user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.update_one(session, id, user)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
return result
|
||||
|
||||
@router.delete('/{id}', response_model=models.UserPublic)
|
||||
def delete_user(id: int, session: Session = Depends(get_session)):
|
||||
def delete_user(
|
||||
id: int,
|
||||
user: models.User = Depends(get_current_user),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
result = service.delete_one(session, id)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"help": "help",
|
||||
"how to use dashboard": "How to use the dashboard",
|
||||
"how to use dashboard": "how to use the dashboard",
|
||||
"product name": "product name",
|
||||
"product price": "product price",
|
||||
"product quantity": "product quantity",
|
||||
"product quantity unit": "product quantity unit",
|
||||
"product type": "product type",
|
||||
"occasional": "occasional",
|
||||
"occasional products": "Occasional products per shipment",
|
||||
"select products per shipment": "Select products for each shipment.",
|
||||
"occasional products": "occasional products per shipment",
|
||||
"select products per shipment": "select products for each shipment.",
|
||||
"recurrent": "recurrent",
|
||||
"recurrent products": "Recurrent products",
|
||||
"recurrent products": "recurrent products",
|
||||
"your selection in this category will apply for all shipments": "your selection will apply to all shipments (Example: For 6 shipments, the product will be counted 6 times: once per shipment).",
|
||||
"product price kg": "product price per kilogram",
|
||||
"product unit": "product sales unit",
|
||||
@@ -82,6 +82,16 @@
|
||||
"templates": "templates",
|
||||
"users": "users",
|
||||
"forms": "contract forms",
|
||||
"form": "contract form",
|
||||
"select a form": "select a form",
|
||||
"download contracts": "download contracts",
|
||||
"all contracts": "all contracts",
|
||||
"remove contract": "remove contract",
|
||||
"download contract": "download contract",
|
||||
"by selecting a form here you can download all contracts of your form": "by selecting a form here you can download all contracts of your form.",
|
||||
"edit user": "edit user",
|
||||
"remove user": "remove user",
|
||||
"logout": "logout",
|
||||
"all forms": "all contract forms",
|
||||
"create new form": "create new contract form",
|
||||
"actions": "actions",
|
||||
@@ -129,7 +139,7 @@
|
||||
"create your contract form, it will create a form in the home page (accessible to users)": "create your contract form in the \"Contract Forms\" section. Adding an entry here will create a form on the home page.",
|
||||
"create shipments for your contract form": "create shipments for your contract",
|
||||
"creation order": "creation order",
|
||||
"dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments": "The dashboard is only visible to referents. You can create your producer, products, contract forms, and shipments.",
|
||||
"dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments": "the dashboard is only visible to referents. You can create your producer, products, contract forms, and shipments.",
|
||||
"is defined by": "is defined by",
|
||||
"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": "a product type defines how it will be organized in the final contract form. It can be recurrent or occasional. Recurrent products will be set for all shipments if selected. Occasional products can be chosen for each shipment.",
|
||||
"and/or": "and/or",
|
||||
@@ -168,7 +178,7 @@
|
||||
"of the shipment": "of the shipment",
|
||||
"of the contract": "of the contract",
|
||||
"login with keycloak": "login with keycloak",
|
||||
"there is no contract for now": "There is no contract at the moment.",
|
||||
"there is no contract for now": "there is no contract at the moment.",
|
||||
"for transfer method contact your referer or productor": "for bank transfer, contact your referent or producer.",
|
||||
"cheque quantity": "number of cheques",
|
||||
"enter cheque quantity": "enter number of cheques",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"product type": "type de produit",
|
||||
"occasional": "occasionnel",
|
||||
"occasional products": "produits occasionnels par livraison",
|
||||
"select products per shipment": "Sélectionnez les produits pour chaque livraison.",
|
||||
"select products per shipment": "sélectionnez les produits pour chaque livraison.",
|
||||
"recurrent": "récurent",
|
||||
"recurrent products": "produits récurrents",
|
||||
"your selection in this category will apply for all shipments": "votre sélection sera appliquée pour chaque livraisons (Exemple: Pour 6 livraisons, le produits sera compté 6 fois : une fois par livraison).",
|
||||
@@ -82,6 +82,15 @@
|
||||
"templates": "modèles",
|
||||
"users": "utilisateur·trices",
|
||||
"forms": "formulaires de contrat",
|
||||
"form": "formulaire de contrat",
|
||||
"select a form": "selectionnez un formulaire",
|
||||
"download contracts": "télécharger les contrats",
|
||||
"all contracts": "tous les contrats",
|
||||
"remove contract": "supprimer le contrat",
|
||||
"download contract": "télécharger le contrat",
|
||||
"by selecting a form here you can download all contracts of your form": "en selectionnant un formulaire, vous téléchargez tous les contrats pour un formulaire donné.",
|
||||
"edit user": "modifier l'utilisateur·trice",
|
||||
"remove user": "supprimer l'utilisateur·trice",
|
||||
"all forms": "tous les formulaires de contrat",
|
||||
"create new form": "créer un nouveau formulaire de contrat",
|
||||
"actions": "actions",
|
||||
@@ -124,12 +133,13 @@
|
||||
"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",
|
||||
"logout": "se déconnecter",
|
||||
"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.",
|
||||
"create shipments for your contract form": "créez les livraisons pour votre contrat",
|
||||
"creation order": "ordre de création",
|
||||
"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 livraison.",
|
||||
"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).",
|
||||
"and/or": "et/ou",
|
||||
@@ -168,10 +178,10 @@
|
||||
"of the shipment": "de la livraison",
|
||||
"of the contract": "du contrat",
|
||||
"login with keycloak": "se connecter avec keycloak",
|
||||
"there is no contract for now": "Il n'y a pas de contrats pour le moment.",
|
||||
"there is no contract for now": "il n'y a pas de contrats pour le moment.",
|
||||
"for transfer method contact your referer or productor": "pour mettre en place le virement automatique, contactez votre référent ou le producteur.",
|
||||
"cheque quantity": "quantité de chèques (pour le paiement en plusieurs fois)",
|
||||
"enter cheque quantity": "Entrez la quantité de chèques",
|
||||
"enter cheque quantity": "entrez la quantité de chèques",
|
||||
"cheque id": "identifiant du chèque",
|
||||
"cheque value": "valeur du chèque",
|
||||
"enter cheque value": "entrez la valeur du chèque",
|
||||
|
||||
20
frontend/src/components/Auth/index.tsx
Normal file
20
frontend/src/components/Auth/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useCurrentUser } from "@/services/api";
|
||||
import { Group, Loader } from "@mantine/core";
|
||||
import { Navigate, Outlet } from "react-router";
|
||||
|
||||
export function Auth() {
|
||||
const { data: userLogged, isLoading } = useCurrentUser();
|
||||
|
||||
if (!userLogged && isLoading)
|
||||
return (
|
||||
<Group align="center" justify="center" h="80vh" w="100%">
|
||||
<Loader color="pink" />
|
||||
</Group>
|
||||
);
|
||||
|
||||
if (!userLogged?.logged) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -1,50 +1,57 @@
|
||||
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
||||
import { Button, Group, Modal, Select, type ModalBaseProps } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||
import { type Contract, type ContractInputs } from "@/services/resources/contracts";
|
||||
import { IconCancel, IconDownload } from "@tabler/icons-react";
|
||||
import { useGetForms } from "@/services/api";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type ContractModalProps = ModalBaseProps & {
|
||||
currentContract?: Contract;
|
||||
handleSubmit: (contract: ContractInputs, id?: number) => void;
|
||||
handleSubmit: (id: number) => void;
|
||||
};
|
||||
|
||||
export function ContractModal({
|
||||
opened,
|
||||
onClose,
|
||||
currentContract,
|
||||
handleSubmit,
|
||||
}: ContractModalProps) {
|
||||
const form = useForm<ContractInputs>({
|
||||
// initialValues: {
|
||||
// firstname: currentContract?.firstname ?? "",
|
||||
// lastname: currentContract?.lastname ?? "",
|
||||
// email: currentContract?.email ?? "",
|
||||
// },
|
||||
// validate: {
|
||||
// firstname: (value) =>
|
||||
// !value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
|
||||
// email: (value) =>
|
||||
// !value ? `${t("email", { capfirst: true })} ${t("is required")}` : null,
|
||||
// },
|
||||
export type ContractDownloadInputs = {
|
||||
form_id: number;
|
||||
};
|
||||
|
||||
export function ContractModal({ opened, onClose, handleSubmit }: ContractModalProps) {
|
||||
const { data: allForms } = useGetForms();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
form_id: null,
|
||||
},
|
||||
validate: {
|
||||
form_id: (value) =>
|
||||
!value ? `${t("a form", { capfirst: true })} ${t("is required")}` : null,
|
||||
},
|
||||
});
|
||||
|
||||
const formSelect = useMemo(() => {
|
||||
return allForms?.map((form) => ({
|
||||
value: String(form.id),
|
||||
label: `${form.season} ${form.name}`,
|
||||
}));
|
||||
}, [allForms]);
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={t("create contract", { capfirst: true })}>
|
||||
<Title order={4}>{t("informations", { capfirst: true })}</Title>
|
||||
<TextInput
|
||||
label={t("contract name", { capfirst: true })}
|
||||
placeholder={t("contract name", { capfirst: true })}
|
||||
radius="sm"
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("download contracts", { capfirst: true })}
|
||||
>
|
||||
<Select
|
||||
label={t("form", { capfirst: true })}
|
||||
placeholder={t("select a form", { capfirst: true })}
|
||||
description={t(
|
||||
"by selecting a form here you can download all contracts of your form",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
nothingFoundMessage={t("nothing found", { capfirst: true })}
|
||||
withAsterisk
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("contract email", { capfirst: true })}
|
||||
placeholder={t("contract email", { capfirst: true })}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps("email")}
|
||||
clearable
|
||||
allowDeselect
|
||||
searchable
|
||||
data={formSelect || []}
|
||||
{...form.getInputProps("form_id")}
|
||||
/>
|
||||
<Group mt="sm" justify="space-between">
|
||||
<Button
|
||||
@@ -61,22 +68,16 @@ export function ContractModal({
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
aria-label={
|
||||
currentContract
|
||||
? t("edit contract", { capfirst: true })
|
||||
: t("create contract", { capfirst: true })
|
||||
}
|
||||
leftSection={currentContract ? <IconEdit /> : <IconPlus />}
|
||||
aria-label={t("download contracts")}
|
||||
leftSection={<IconDownload />}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
if (form.isValid()) {
|
||||
handleSubmit(form.getValues(), currentContract?.id);
|
||||
handleSubmit(Number(form.values.form_id));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentContract
|
||||
? t("edit contract", { capfirst: true })
|
||||
: t("create contract", { capfirst: true })}
|
||||
{t("download contracts", { capfirst: true })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
|
||||
@@ -10,24 +10,17 @@ export type ContractRowProps = {
|
||||
};
|
||||
|
||||
export default function ContractRow({ contract }: ContractRowProps) {
|
||||
// const [searchParams] = useSearchParams();
|
||||
const deleteMutation = useDeleteContract();
|
||||
// const navigate = useNavigate();
|
||||
const {refetch, isFetching} = useGetContractFile(contract.id)
|
||||
const handleDownload = useCallback(async () => {
|
||||
const { data } = await refetch();
|
||||
if (!data)
|
||||
return;
|
||||
const getContractMutation = useGetContractFile();
|
||||
|
||||
const url = URL.createObjectURL(data.blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = data.filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [useGetContractFile])
|
||||
const handleDownload = useCallback(async () => {
|
||||
getContractMutation.mutateAsync(contract.id);
|
||||
}, [useGetContractFile, contract, contract.id]);
|
||||
return (
|
||||
<Table.Tr key={contract.id}>
|
||||
<Table.Td>
|
||||
{contract.form.name} {contract.form.season}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{contract.firstname} {contract.lastname}
|
||||
</Table.Td>
|
||||
@@ -36,32 +29,16 @@ export default function ContractRow({ contract }: ContractRowProps) {
|
||||
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{/* <Tooltip label={t("edit contract", { capfirst: true })}>
|
||||
<Tooltip label={t("download contract", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
mr="5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/${contract.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
handleDownload();
|
||||
}}
|
||||
>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip> */}
|
||||
<Tooltip
|
||||
label={t("download contract")}
|
||||
>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
mr="5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload()
|
||||
}}
|
||||
>
|
||||
<IconDownload/>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("remove contract", { capfirst: true })}>
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
import { NavLink } from "react-router";
|
||||
import { t } from "@/config/i18n";
|
||||
import "./index.css";
|
||||
import { Group } from "@mantine/core";
|
||||
import { Button, Group, Loader } from "@mantine/core";
|
||||
import { Config } from "@/config/config";
|
||||
import { useCurrentUser, useLogoutUser } from "@/services/api";
|
||||
|
||||
export function Navbar() {
|
||||
const { data: user, isLoading } = useCurrentUser();
|
||||
const logout = useLogoutUser();
|
||||
if (!user && isLoading) {
|
||||
return (
|
||||
<Group align="center" justify="center" h="80vh" w="100%">
|
||||
<Loader color="pink" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<Group>
|
||||
<NavLink className={"navLink"} aria-label={t("home")} to="/">
|
||||
{t("home", { capfirst: true })}
|
||||
</NavLink>
|
||||
{user?.logged ? (
|
||||
<NavLink className={"navLink"} aria-label={t("dashboard")} to="/dashboard/help">
|
||||
{t("dashboard", { capfirst: true })}
|
||||
</NavLink>
|
||||
) : null}
|
||||
</Group>
|
||||
{!user?.logged ? (
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
aria-label={t("dashboard")}
|
||||
to="/dashboard/help"
|
||||
aria-label={t("login with keycloak", { capfirst: true })}
|
||||
to={`${Config.backend_uri}/auth/login`}
|
||||
>
|
||||
{t("dashboard", { capfirst: true })}
|
||||
{t("login with keycloak", { capfirst: true })}
|
||||
</NavLink>
|
||||
</Group>
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
aria-label={t("login with keycloak", { capfirst: true })}
|
||||
to={`${Config.backend_uri}/auth/login`}
|
||||
>
|
||||
{t("login with keycloak", { capfirst: true })}
|
||||
</NavLink>
|
||||
) : (
|
||||
<Button
|
||||
className={"navLink"}
|
||||
aria-label={t("logout", { capfirst: true })}
|
||||
onClick={() => {
|
||||
logout.mutate();
|
||||
}}
|
||||
>
|
||||
{t("logout", { capfirst: true })}
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Group,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
Select,
|
||||
TextInput,
|
||||
Title,
|
||||
type ModalBaseProps,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
type Productor,
|
||||
type ProductorInputs,
|
||||
} from "@/services/resources/productors";
|
||||
import { useMemo } from "react";
|
||||
import { useGetRoles } from "@/services/api";
|
||||
|
||||
export type ProductorModalProps = ModalBaseProps & {
|
||||
currentProductor?: Productor;
|
||||
@@ -27,6 +30,8 @@ export function ProductorModal({
|
||||
currentProductor,
|
||||
handleSubmit,
|
||||
}: ProductorModalProps) {
|
||||
const { data: allRoles } = useGetRoles();
|
||||
|
||||
const form = useForm<ProductorInputs>({
|
||||
initialValues: {
|
||||
name: currentProductor?.name ?? "",
|
||||
@@ -44,6 +49,10 @@ export function ProductorModal({
|
||||
},
|
||||
});
|
||||
|
||||
const roleSelect = useMemo(() => {
|
||||
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
|
||||
}, [allRoles]);
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={t("create productor", { capfirst: true })}>
|
||||
<Title order={4}>{t("Informations", { capfirst: true })}</Title>
|
||||
@@ -54,11 +63,14 @@ export function ProductorModal({
|
||||
withAsterisk
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
<Select
|
||||
label={t("productor type", { capfirst: true })}
|
||||
placeholder={t("productor type", { capfirst: true })}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
clearable
|
||||
searchable
|
||||
data={roleSelect || []}
|
||||
{...form.getInputProps("type")}
|
||||
/>
|
||||
<TextInput
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from "@/services/resources/products";
|
||||
import { useMemo } from "react";
|
||||
import { useGetProductors } from "@/services/api";
|
||||
import { InputLabel } from "@/components/Label";
|
||||
|
||||
export type ProductModalProps = ModalBaseProps & {
|
||||
currentProduct?: Product;
|
||||
@@ -90,7 +89,10 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
|
||||
label={t("product type", { capfirst: true })}
|
||||
placeholder={t("product type", { capfirst: true })}
|
||||
radius="sm"
|
||||
description={t("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", {capfirst: true})}
|
||||
description={t(
|
||||
"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",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
searchable
|
||||
clearable
|
||||
withAsterisk
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
||||
import {
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
Select,
|
||||
TextInput,
|
||||
Title,
|
||||
type ModalBaseProps,
|
||||
} from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||
import { type User, type UserInputs } from "@/services/resources/users";
|
||||
import { useGetRoles } from "@/services/api";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type UserModalProps = ModalBaseProps & {
|
||||
currentUser?: User;
|
||||
@@ -10,10 +21,12 @@ export type UserModalProps = ModalBaseProps & {
|
||||
};
|
||||
|
||||
export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserModalProps) {
|
||||
const { data: allRoles } = useGetRoles();
|
||||
const form = useForm<UserInputs>({
|
||||
initialValues: {
|
||||
name: currentUser?.name ?? "",
|
||||
email: currentUser?.email ?? "",
|
||||
role_names: currentUser?.roles.map((role) => role.name) ?? [],
|
||||
},
|
||||
validate: {
|
||||
name: (value) =>
|
||||
@@ -23,6 +36,10 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
|
||||
},
|
||||
});
|
||||
|
||||
const roleSelect = useMemo(() => {
|
||||
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
|
||||
}, [allRoles]);
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title={t("create user", { capfirst: true })}>
|
||||
<Title order={4}>{t("informations", { capfirst: true })}</Title>
|
||||
@@ -40,6 +57,16 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
|
||||
withAsterisk
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={t("user roles", { capfirst: true })}
|
||||
placeholder={t("user roles", { capfirst: true })}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
clearable
|
||||
searchable
|
||||
data={roleSelect || []}
|
||||
{...form.getInputProps("role_names")}
|
||||
/>
|
||||
<Group mt="sm" justify="space-between">
|
||||
<Button
|
||||
variant="filled"
|
||||
|
||||
@@ -250,8 +250,8 @@ export function Contract() {
|
||||
) : null}
|
||||
{shipments.some((shipment) => shipment.products.length > 0) ? (
|
||||
<>
|
||||
<Title order={3}>{t("occasional products")}</Title>
|
||||
<Text>{t("select products per shipment")}</Text>
|
||||
<Title order={3}>{t("occasional products", { capfirst: true })}</Title>
|
||||
<Text>{t("select products per shipment", { capfirst: true })}</Text>
|
||||
<Accordion defaultValue={"0"}>
|
||||
{shipments.map((shipment, index) => (
|
||||
<ShipmentForm
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useCreateContract, useGetContract, useGetContracts } from "@/services/api";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { useGetAllContractFile, useGetContracts } from "@/services/api";
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import ContractRow from "@/components/Contracts/Row";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { ContractModal } from "@/components/Contracts/Modal";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { type Contract, type ContractInputs } from "@/services/resources/contracts";
|
||||
import { type Contract } from "@/services/resources/contracts";
|
||||
import ContractsFilters from "@/components/Contracts/Filter";
|
||||
|
||||
export default function Contracts() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// const location = useLocation();
|
||||
// const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const getAllContractFilesMutation = useGetAllContractFile();
|
||||
const isdownload = location.pathname.includes("/download");
|
||||
|
||||
// const isCreate = location.pathname === "/dashboard/contracts/create";
|
||||
// const isEdit = location.pathname.includes("/edit");
|
||||
|
||||
// const editId = useMemo(() => {
|
||||
// if (isEdit) {
|
||||
// return location.pathname.split("/")[3];
|
||||
// }
|
||||
// return null;
|
||||
// }, [location, isEdit]);
|
||||
|
||||
// const closeModal = useCallback(() => {
|
||||
// navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
// }, [navigate, searchParams]);
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
const { data: contracts, isPending } = useGetContracts(searchParams);
|
||||
// const { data: currentContract } = useGetContract(Number(editId), {
|
||||
// enabled: !!editId,
|
||||
// });
|
||||
|
||||
const { data: allContracts } = useGetContracts();
|
||||
|
||||
@@ -56,6 +45,13 @@ export default function Contracts() {
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const handleDownloadContracts = useCallback(
|
||||
async (id: number) => {
|
||||
await getAllContractFilesMutation.mutateAsync(id);
|
||||
},
|
||||
[getAllContractFilesMutation],
|
||||
);
|
||||
|
||||
if (!contracts || isPending)
|
||||
return (
|
||||
<Group align="center" justify="center" h="80vh" w="100%">
|
||||
@@ -66,32 +62,24 @@ export default function Contracts() {
|
||||
return (
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
|
||||
{/* <Tooltip label={t("create contract", { capfirst: true })}>
|
||||
<Title order={2}>{t("all contracts", { capfirst: true })}</Title>
|
||||
<Tooltip label={t("download contracts", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/create${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconPlus />
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ContractModal
|
||||
key={`${currentContract?.id}_create`}
|
||||
opened={isCreate}
|
||||
opened={isdownload}
|
||||
onClose={closeModal}
|
||||
handleSubmit={handleCreateContract}
|
||||
handleSubmit={handleDownloadContracts}
|
||||
/>
|
||||
<ContractModal
|
||||
key={`${currentContract?.id}_edit`}
|
||||
opened={isEdit}
|
||||
onClose={closeModal}
|
||||
currentContract={currentContract}
|
||||
handleSubmit={handleEditContract}
|
||||
/> */}
|
||||
</Group>
|
||||
<ContractsFilters
|
||||
forms={forms || []}
|
||||
@@ -102,6 +90,7 @@ export default function Contracts() {
|
||||
<Table striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("form", { capfirst: true })}</Table.Th>
|
||||
<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>
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
import { t } from "@/config/i18n";
|
||||
import { Accordion, ActionIcon, Blockquote, Group, NumberInput, Paper, Select, Stack, TableOfContents, Text, TextInput, Title } from "@mantine/core";
|
||||
import { IconEdit, IconInfoCircle, IconLink, IconPlus, IconTestPipe, IconX } from "@tabler/icons-react";
|
||||
import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Blockquote,
|
||||
Group,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TableOfContents,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconEdit,
|
||||
IconInfoCircle,
|
||||
IconLink,
|
||||
IconPlus,
|
||||
IconTestPipe,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function Help() {
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={2}>{t("how to use dashboard", {capfirst: true})}</Title>
|
||||
<Title order={2}>{t("how to use dashboard", { capfirst: true })}</Title>
|
||||
<Text>
|
||||
{t("dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments", {capfirst: true})}
|
||||
{t(
|
||||
"dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<TableOfContents
|
||||
variant="filled"
|
||||
@@ -16,144 +39,255 @@ export function Help() {
|
||||
size="sm"
|
||||
radius="sm"
|
||||
scrollSpyOptions={{
|
||||
selector: 'h3, h4',
|
||||
selector: "h3, h4",
|
||||
}}
|
||||
getControlProps={({ data }) => ({
|
||||
onClick: () => data.getNode().scrollIntoView(),
|
||||
children: data.value,
|
||||
})}
|
||||
/>
|
||||
<Title order={3}>{t("creation order", {capfirst: true})}</Title>
|
||||
<Title order={3}>{t("creation order", { capfirst: true })}</Title>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("a productor", {capfirst: true})}</Title>
|
||||
<Title order={4}>{t("a productor", { capfirst: true })}</Title>
|
||||
<Text>
|
||||
{t("start to create a productor in the productors section", {capfirst: true})}
|
||||
{t("start to create a productor in the productors section", { capfirst: true })}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/productors"
|
||||
aria-label={t("link to the section", {capfirst: true, section: t("productors")})}
|
||||
style={{cursor: "pointer", alignSelf: "center"}}
|
||||
><IconLink/></ActionIcon>
|
||||
</Text>
|
||||
<Text>{t("a productor can be edited if its informations change, it should not be recreated for each contracts", {capfirst: true})}</Text>
|
||||
<Blockquote
|
||||
mt="md"
|
||||
icon={<IconInfoCircle/>}
|
||||
>
|
||||
<Text>{t("to add a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("productors")})}</Text>
|
||||
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
|
||||
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("the products", {capfirst: true})}</Title>
|
||||
<Text>
|
||||
{t("add all products linked to this productor in the products section", {capfirst: true})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {capfirst: true, section: t("products")})}
|
||||
style={{cursor: "pointer", alignSelf: "center"}}
|
||||
><IconLink/></ActionIcon>
|
||||
</Text>
|
||||
<Text>{t("a product can be edited if its informations change, it should not be recreated for each contracts", {capfirst: true})}</Text>
|
||||
<Blockquote
|
||||
mt="md"
|
||||
icon={<IconInfoCircle/>}
|
||||
>
|
||||
<Text>{t("to add a use the", {capfirst: true, section: t("a product")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("products")})}</Text>
|
||||
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
|
||||
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("a form", {capfirst: true})}</Title>
|
||||
<Text>
|
||||
{t("create your contract form, it will create a form in the home page (accessible to users)", {capfirst: true})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {capfirst: true, section: t("forms")})}
|
||||
style={{cursor: "pointer", alignSelf: "center"}}
|
||||
><IconLink/></ActionIcon>
|
||||
</Text>
|
||||
<Text>{t("a new contract form should be created for each new season, do not edit a previous contract and change it's values (for history purpose)", {capfirst: true})}</Text>
|
||||
<Blockquote
|
||||
mt="md"
|
||||
icon={<IconInfoCircle/>}
|
||||
>
|
||||
<Text>{t("to add a use the", {capfirst: true, section: t("a form")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("forms")})}</Text>
|
||||
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
|
||||
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("the shipments", {capfirst: true})}</Title>
|
||||
<Text>
|
||||
{t("create shipments for your contract form", {capfirst: true})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {capfirst: true, section: t("shipments")})}
|
||||
style={{cursor: "pointer", alignSelf: "center"}}
|
||||
><IconLink/></ActionIcon>
|
||||
</Text>
|
||||
<Text>
|
||||
{t("all shipments should be recreated for each form creation", {capfirst: true})}
|
||||
</Text>
|
||||
<Blockquote
|
||||
mt="md"
|
||||
icon={<IconInfoCircle/>}
|
||||
>
|
||||
<Text>{t("to add a use the", {capfirst: true, section: t("a shipment")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("shipments")})}</Text>
|
||||
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
|
||||
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={3}>{t("glossary", {capfirst: true})}</Title>
|
||||
<Stack>
|
||||
<Title order={4} fw={700}>{t("product type", {capfirst: true})}</Title>
|
||||
<Text>{t("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", {capfirst: true})}</Text>
|
||||
<Stack>
|
||||
<Text>{t("example in user forms", {capfirst: true})} ({t("recurrent product")}) :</Text>
|
||||
<Blockquote
|
||||
color="black"
|
||||
icon={<IconTestPipe/>}
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("productors"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<Title order={5}>{t('recurrent products')}</Title>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>
|
||||
{t(
|
||||
"a productor can be edited if its informations change, it should not be recreated for each contracts",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Blockquote mt="md" icon={<IconInfoCircle />}>
|
||||
<Text>
|
||||
{t("to add a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("productors") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconEdit />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to edit")}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm" color="red">
|
||||
<IconX />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to delete")}
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("the products", { capfirst: true })}</Title>
|
||||
<Text>
|
||||
{t("add all products linked to this productor in the products section", {
|
||||
capfirst: true,
|
||||
})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("products"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>
|
||||
{t(
|
||||
"a product can be edited if its informations change, it should not be recreated for each contracts",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Blockquote mt="md" icon={<IconInfoCircle />}>
|
||||
<Text>
|
||||
{t("to add a use the", { capfirst: true, section: t("a product") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("products") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconEdit />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to edit")}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm" color="red">
|
||||
<IconX />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to delete")}
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("a form", { capfirst: true })}</Title>
|
||||
<Text>
|
||||
{t(
|
||||
"create your contract form, it will create a form in the home page (accessible to users)",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("forms"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>
|
||||
{t(
|
||||
"a new contract form should be created for each new season, do not edit a previous contract and change it's values (for history purpose)",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Blockquote mt="md" icon={<IconInfoCircle />}>
|
||||
<Text>
|
||||
{t("to add a use the", { capfirst: true, section: t("a form") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("forms") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconEdit />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to edit")}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm" color="red">
|
||||
<IconX />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to delete")}
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Stack gap="1">
|
||||
<Title order={4}>{t("the shipments", { capfirst: true })}</Title>
|
||||
<Text>
|
||||
{t("create shipments for your contract form", { capfirst: true })}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/products"
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("shipments"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>
|
||||
{t("all shipments should be recreated for each form creation", {
|
||||
capfirst: true,
|
||||
})}
|
||||
</Text>
|
||||
<Blockquote mt="md" icon={<IconInfoCircle />}>
|
||||
<Text>
|
||||
{t("to add a use the", { capfirst: true, section: t("a shipment") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("shipments") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconEdit />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to edit")}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
<ActionIcon size="sm" color="red">
|
||||
<IconX />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in front of the line you want to delete")}
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={3}>{t("glossary", { capfirst: true })}</Title>
|
||||
<Stack>
|
||||
<Title order={4} fw={700}>
|
||||
{t("product type", { capfirst: true })}
|
||||
</Title>
|
||||
<Text>
|
||||
{t(
|
||||
"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",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Stack>
|
||||
<Text>
|
||||
{t("example in user forms", { capfirst: true })} ({t("recurrent product")})
|
||||
:
|
||||
</Text>
|
||||
<Blockquote color="black" icon={<IconTestPipe />}>
|
||||
<Title order={5}>{t("recurrent products", { capfirst: true })}</Title>
|
||||
<Text size="sm">
|
||||
{t("your selection in this category will apply for all shipments", {
|
||||
capfirst: true,
|
||||
})}
|
||||
</Text>
|
||||
<NumberInput
|
||||
label={t("product example", {capfirst: true})}
|
||||
label={t("product example", { capfirst: true })}
|
||||
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
aria-label={t("enter quantity")}
|
||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
/>
|
||||
</Blockquote>
|
||||
<Text>{t("example in user forms", {capfirst: true})} ({t("occasional product")}) :</Text>
|
||||
<Blockquote
|
||||
color="black"
|
||||
icon={<IconTestPipe/>}
|
||||
>
|
||||
<Title order={5}>{t('occasional products')}</Title>
|
||||
<Text>{t("select products per shipment")}</Text>
|
||||
<Text>
|
||||
{t("example in user forms", { capfirst: true })} ({t("occasional product")})
|
||||
:
|
||||
</Text>
|
||||
<Blockquote color="black" icon={<IconTestPipe />}>
|
||||
<Title order={5}>{t("occasional products", { capfirst: true })}</Title>
|
||||
<Text>{t("select products per shipment", { capfirst: true })}</Text>
|
||||
<Accordion defaultValue={"0"}>
|
||||
<Accordion.Item value={"example1"}>
|
||||
<Accordion.Control>{t("shipment", {capfirst: true})} 1</Accordion.Control>
|
||||
<Accordion.Control>
|
||||
{t("shipment", { capfirst: true })} 1
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<NumberInput
|
||||
label={t("product example", {capfirst: true})}
|
||||
label={t("product example", { capfirst: true })}
|
||||
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
aria-label={t("enter quantity")}
|
||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
@@ -161,10 +295,12 @@ export function Help() {
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value={"example2"}>
|
||||
<Accordion.Control>{t("shipment", {capfirst: true})} 2</Accordion.Control>
|
||||
<Accordion.Control>
|
||||
{t("shipment", { capfirst: true })} 2
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<NumberInput
|
||||
label={t("product example", {capfirst: true})}
|
||||
label={t("product example", { capfirst: true })}
|
||||
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
aria-label={t("enter quantity")}
|
||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
|
||||
@@ -174,58 +310,84 @@ export function Help() {
|
||||
</Accordion>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={4} fw={700}>{t("sell unit", {capfirst: true})}</Title>
|
||||
<Text>{t("the product unit will be assigned to the quantity requested in the form")}</Text>
|
||||
<Title order={4} fw={700}>
|
||||
{t("sell unit", { capfirst: true })}
|
||||
</Title>
|
||||
<Text>
|
||||
{t("the product unit will be assigned to the quantity requested in the form")}
|
||||
</Text>
|
||||
<Stack w={"100%"}>
|
||||
<Text>{t("example in user forms", {capfirst: true})} ({t("with grams as product unit selected")}) :</Text>
|
||||
<Blockquote
|
||||
color="black"
|
||||
icon={<IconTestPipe/>}
|
||||
>
|
||||
<Text>
|
||||
{t("example in user forms", { capfirst: true })} (
|
||||
{t("with grams as product unit selected")}) :
|
||||
</Text>
|
||||
<Blockquote color="black" icon={<IconTestPipe />}>
|
||||
<NumberInput
|
||||
label={t("product example", {capfirst: true})}
|
||||
label={t("product example", { capfirst: true })}
|
||||
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("grams")}`}
|
||||
aria-label={t("enter quantity")}
|
||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("grams")}`}
|
||||
/>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={4} fw={700}>{t("payment methods", {capfirst: true})}</Title>
|
||||
<Text>{t("payment methods are defined for a productor. At the end of a form a section payment method let the user select his prefered payment method", {capfirst: true})}</Text>
|
||||
<Title order={4} fw={700}>
|
||||
{t("payment methods", { capfirst: true })}
|
||||
</Title>
|
||||
<Text>
|
||||
{t(
|
||||
"payment methods are defined for a productor. At the end of a form a section payment method let the user select his prefered payment method",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Stack>
|
||||
<Text>{t("example in user forms", {capfirst: true})} ({t("with cheque and transfer")}) :</Text>
|
||||
<Blockquote
|
||||
color="black"
|
||||
icon={<IconTestPipe/>}
|
||||
>
|
||||
<Text>
|
||||
{t("example in user forms", { capfirst: true })} (
|
||||
{t("with cheque and transfer")}) :
|
||||
</Text>
|
||||
<Blockquote color="black" icon={<IconTestPipe />}>
|
||||
<Title order={5}>{t("payment method", { capfirst: true })}</Title>
|
||||
<Select
|
||||
label={t("payment method", { capfirst: true })}
|
||||
placeholder={t("enter payment method", { capfirst: true })}
|
||||
description={t("choose payment method", { capfirst: true })}
|
||||
data={[t("cheque", {capfirst: true}), t("transfer", {capfirst: true})]}
|
||||
data={[
|
||||
t("cheque", { capfirst: true }),
|
||||
t("transfer", { capfirst: true }),
|
||||
]}
|
||||
/>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={4} fw={700}>{t("product quantity", {capfirst: true})}</Title>
|
||||
<Text>{t("this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form", {capfirst: true})}</Text>
|
||||
<Title order={4} fw={700}>{t("product quantity unit", {capfirst: true})}</Title>
|
||||
<Text>{t("this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form", {capfirst: true})}</Text>
|
||||
<Text>{t("example in user forms", {capfirst: true})} ({t("with 150 set as quantity and g as quantity unit in product")}):</Text>
|
||||
<Blockquote
|
||||
color="black"
|
||||
icon={<IconTestPipe/>}
|
||||
>
|
||||
<Title order={4} fw={700}>
|
||||
{t("product quantity", { capfirst: true })}
|
||||
</Title>
|
||||
<Text>
|
||||
{t(
|
||||
"this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Title order={4} fw={700}>
|
||||
{t("product quantity unit", { capfirst: true })}
|
||||
</Title>
|
||||
<Text>
|
||||
{t(
|
||||
"this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form",
|
||||
{ capfirst: true },
|
||||
)}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("example in user forms", { capfirst: true })} (
|
||||
{t("with 150 set as quantity and g as quantity unit in product")}):
|
||||
</Text>
|
||||
<Blockquote color="black" icon={<IconTestPipe />}>
|
||||
<NumberInput
|
||||
label={`${t("product example", {capfirst: true})} 150g`}
|
||||
label={`${t("product example", { capfirst: true })} 150g`}
|
||||
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("pieces")}`}
|
||||
aria-label={t("enter quantity")}
|
||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("pieces")}`}
|
||||
/>
|
||||
</Blockquote>
|
||||
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
22
frontend/src/pages/Login/index.tsx
Normal file
22
frontend/src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Loader } from "@mantine/core";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
export function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const accessToken = searchParams.get("access_token");
|
||||
const idToken = searchParams.get("id_token");
|
||||
const refreshToken = searchParams.get("refresh_token");
|
||||
|
||||
if (accessToken && idToken) {
|
||||
localStorage.setItem("access_token", accessToken);
|
||||
localStorage.setItem("id_token", idToken);
|
||||
localStorage.setItem("refresh_token", refreshToken || "");
|
||||
window.location.href = "/";
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return <Loader />;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import { Contract } from "./pages/Contract";
|
||||
import { NotFound } from "./pages/NotFound";
|
||||
import Contracts from "./pages/Contracts";
|
||||
import { Help } from "./pages/Help";
|
||||
import { Login } from "./pages/Login";
|
||||
import { Auth } from "./components/Auth";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
@@ -22,29 +24,36 @@ export const router = createBrowserRouter([
|
||||
{ index: true, Component: Home },
|
||||
{ path: "/forms", Component: Forms },
|
||||
{
|
||||
path: "/dashboard",
|
||||
Component: Dashboard,
|
||||
element: <Auth />,
|
||||
children: [
|
||||
{ path: "help", Component: Help },
|
||||
{ path: "productors", Component: Productors },
|
||||
{ path: "productors/create", Component: Productors },
|
||||
{ path: "productors/:id/edit", Component: Productors },
|
||||
{ path: "products", Component: Products },
|
||||
{ path: "products/create", Component: Products },
|
||||
{ path: "products/:id/edit", Component: Products },
|
||||
{ path: "contracts", Component: Contracts },
|
||||
{ path: "users", Component: Users },
|
||||
{ path: "users/create", Component: Users },
|
||||
{ path: "users/:id/edit", Component: Users },
|
||||
{ path: "forms", Component: Forms },
|
||||
{ path: "forms/:id/edit", Component: Forms },
|
||||
{ path: "forms/create", Component: Forms },
|
||||
{ path: "shipments", Component: Shipments },
|
||||
{ path: "shipments/:id/edit", Component: Shipments },
|
||||
{ path: "shipments/create", Component: Shipments },
|
||||
{
|
||||
path: "/dashboard",
|
||||
Component: Dashboard,
|
||||
children: [
|
||||
{ path: "help", Component: Help },
|
||||
{ path: "productors", Component: Productors },
|
||||
{ path: "productors/create", Component: Productors },
|
||||
{ path: "productors/:id/edit", Component: Productors },
|
||||
{ path: "products", Component: Products },
|
||||
{ path: "products/create", Component: Products },
|
||||
{ path: "products/:id/edit", Component: Products },
|
||||
{ path: "contracts", Component: Contracts },
|
||||
{ path: "contracts/download", Component: Contracts },
|
||||
{ path: "users", Component: Users },
|
||||
{ path: "users/create", Component: Users },
|
||||
{ path: "users/:id/edit", Component: Users },
|
||||
{ path: "forms", Component: Forms },
|
||||
{ path: "forms/:id/edit", Component: Forms },
|
||||
{ path: "forms/create", Component: Forms },
|
||||
{ path: "shipments", Component: Shipments },
|
||||
{ path: "shipments/:id/edit", Component: Shipments },
|
||||
{ path: "shipments/create", Component: Shipments },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "/form/:id", Component: Contract },
|
||||
{ path: "/auth/login", Component: Login },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -13,7 +13,13 @@ import type {
|
||||
ProductorCreate,
|
||||
ProductorEditPayload,
|
||||
} from "@/services/resources/productors";
|
||||
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
|
||||
import type {
|
||||
Role,
|
||||
User,
|
||||
UserCreate,
|
||||
UserEditPayload,
|
||||
UserLogged,
|
||||
} from "@/services/resources/users";
|
||||
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
|
||||
import type { Contract, ContractCreate } from "./resources/contracts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
@@ -24,9 +30,9 @@ export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipm
|
||||
return useQuery<Shipment[]>({
|
||||
queryKey: ["shipments", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`).then(
|
||||
(res) => res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,7 +42,10 @@ export function useGetShipment(
|
||||
): UseQueryResult<Shipment, Error> {
|
||||
return useQuery<Shipment>({
|
||||
queryKey: ["shipment"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/shipments/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/shipments/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
@@ -49,6 +58,7 @@ export function useCreateShipment() {
|
||||
mutationFn: (newShipment: ShipmentCreate) => {
|
||||
return fetch(`${Config.backend_uri}/shipments`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -65,7 +75,9 @@ export function useCreateShipment() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the shipment") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error create`, { capfirst: true, entity: t("of the shipment") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -79,6 +91,7 @@ export function useEditShipment() {
|
||||
mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
|
||||
return fetch(`${Config.backend_uri}/shipments/${id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -95,7 +108,9 @@ export function useEditShipment() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the shipment") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error edit`, { capfirst: true, entity: t("of the shipment") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -108,6 +123,7 @@ export function useDeleteShipment() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/shipments/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -123,7 +139,9 @@ export function useDeleteShipment() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the contract") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the contract") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -135,9 +153,9 @@ export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Prod
|
||||
return useQuery<Productor[]>({
|
||||
queryKey: ["productors", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`).then(
|
||||
(res) => res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +165,10 @@ export function useGetProductor(
|
||||
) {
|
||||
return useQuery<Productor>({
|
||||
queryKey: ["productor"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/productors/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/productors/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
@@ -160,6 +181,7 @@ export function useCreateProductor() {
|
||||
mutationFn: (newProductor: ProductorCreate) => {
|
||||
return fetch(`${Config.backend_uri}/productors`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -176,7 +198,9 @@ export function useCreateProductor() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the productor") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error create`, { capfirst: true, entity: t("of the productor") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -190,6 +214,7 @@ export function useEditProductor() {
|
||||
mutationFn: ({ productor, id }: ProductorEditPayload) => {
|
||||
return fetch(`${Config.backend_uri}/productors/${id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -206,7 +231,9 @@ export function useEditProductor() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the productor") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error edit`, { capfirst: true, entity: t("of the productor") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -219,6 +246,7 @@ export function useDeleteProductor() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/productors/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -234,7 +262,9 @@ export function useDeleteProductor() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the productor") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the productor") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -247,7 +277,10 @@ export function useGetForm(
|
||||
) {
|
||||
return useQuery<Form>({
|
||||
queryKey: ["form"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/forms/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
@@ -258,9 +291,9 @@ export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], E
|
||||
return useQuery<Form[]>({
|
||||
queryKey: ["forms", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) =>
|
||||
res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -271,6 +304,7 @@ export function useCreateForm() {
|
||||
mutationFn: (newForm: FormCreate) => {
|
||||
return fetch(`${Config.backend_uri}/forms`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -289,6 +323,7 @@ export function useDeleteForm() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/forms/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -304,7 +339,9 @@ export function useDeleteForm() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the form") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the form") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -318,6 +355,7 @@ export function useEditForm() {
|
||||
mutationFn: ({ id, form }: FormEditPayload) => {
|
||||
return fetch(`${Config.backend_uri}/forms/${id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -334,7 +372,8 @@ export function useEditForm() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the form") }),
|
||||
message:
|
||||
error?.message || t(`error edit`, { capfirst: true, entity: t("of the form") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -347,7 +386,10 @@ export function useGetProduct(
|
||||
) {
|
||||
return useQuery<Product>({
|
||||
queryKey: ["product"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/products/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/products/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
@@ -358,9 +400,9 @@ export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Produc
|
||||
return useQuery<Product[]>({
|
||||
queryKey: ["products", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`).then((res) =>
|
||||
res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,6 +413,7 @@ export function useCreateProduct() {
|
||||
mutationFn: (newProduct: ProductCreate) => {
|
||||
return fetch(`${Config.backend_uri}/products`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -387,7 +430,9 @@ export function useCreateProduct() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the productor") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error create`, { capfirst: true, entity: t("of the productor") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -400,6 +445,7 @@ export function useDeleteProduct() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/products/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -415,7 +461,9 @@ export function useDeleteProduct() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the product") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the product") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -429,6 +477,7 @@ export function useEditProduct() {
|
||||
mutationFn: ({ id, product }: ProductEditPayload) => {
|
||||
return fetch(`${Config.backend_uri}/products/${id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -445,7 +494,9 @@ export function useEditProduct() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the product") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error edit`, { capfirst: true, entity: t("of the product") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -458,7 +509,10 @@ export function useGetUser(
|
||||
) {
|
||||
return useQuery<User>({
|
||||
queryKey: ["user"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/users/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/users/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
@@ -469,9 +523,9 @@ export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], E
|
||||
return useQuery<User[]>({
|
||||
queryKey: ["users", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`).then((res) =>
|
||||
res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -482,6 +536,7 @@ export function useCreateUser() {
|
||||
mutationFn: (newUser: UserCreate) => {
|
||||
return fetch(`${Config.backend_uri}/users`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -498,7 +553,9 @@ export function useCreateUser() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the user") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error create`, { capfirst: true, entity: t("of the user") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -511,6 +568,7 @@ export function useDeleteUser() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/users/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -526,7 +584,9 @@ export function useDeleteUser() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the user") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the user") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -540,6 +600,7 @@ export function useEditUser() {
|
||||
mutationFn: ({ id, user }: UserEditPayload) => {
|
||||
return fetch(`${Config.backend_uri}/users/${id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -556,7 +617,8 @@ export function useEditUser() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the user") }),
|
||||
message:
|
||||
error?.message || t(`error edit`, { capfirst: true, entity: t("of the user") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
@@ -568,9 +630,9 @@ export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contr
|
||||
return useQuery<Contract[]>({
|
||||
queryKey: ["contracts", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`).then(
|
||||
(res) => res.json(),
|
||||
),
|
||||
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -580,28 +642,68 @@ export function useGetContract(
|
||||
) {
|
||||
return useQuery<Contract>({
|
||||
queryKey: ["contract"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}`).then((res) => res.json()),
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/contracts/${id}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetContractFile() {
|
||||
return useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
const res = await fetch(`${Config.backend_uri}/contracts/${id}/file`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res);
|
||||
|
||||
export function useGetContractFile(
|
||||
id?: number,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ["contract"],
|
||||
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}/file`)
|
||||
.then(async (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("filname=")[1].replace(/"/g, "") :
|
||||
`contract_${id}.pdf`
|
||||
return {blob, filename};
|
||||
}),
|
||||
enabled: !!id,
|
||||
const filename =
|
||||
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 useGetAllContractFile() {
|
||||
return useMutation({
|
||||
mutationFn: async (form_id: number) => {
|
||||
const res = await fetch(`${Config.backend_uri}/contracts/${form_id}/files`, {
|
||||
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_${form_id}.zip`;
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -612,6 +714,7 @@ export function useCreateContract() {
|
||||
mutationFn: (newContract: ContractCreate) => {
|
||||
return fetch(`${Config.backend_uri}/contracts`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -636,6 +739,7 @@ export function useDeleteContract() {
|
||||
mutationFn: (id: number) => {
|
||||
return fetch(`${Config.backend_uri}/contracts/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -651,9 +755,54 @@ export function useDeleteContract() {
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: t("error", { capfirst: true }),
|
||||
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the contract") }),
|
||||
message:
|
||||
error?.message ||
|
||||
t(`error delete`, { capfirst: true, entity: t("of the contract") }),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetRoles(filters?: URLSearchParams): UseQueryResult<Role[], Error> {
|
||||
const queryString = filters?.toString();
|
||||
return useQuery<Role[]>({
|
||||
queryKey: ["roles", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCurrentUser() {
|
||||
return useQuery<UserLogged>({
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: () => {
|
||||
return fetch(`${Config.backend_uri}/auth/user/me`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogoutUser() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => {
|
||||
return fetch(`${Config.backend_uri}/auth/logout`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import type { Product } from "@/services/resources/products";
|
||||
|
||||
export type UserLogged = {
|
||||
logged: boolean;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export type Role = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
products: Product[];
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export type UserInputs = {
|
||||
email: string;
|
||||
name: string;
|
||||
role_names: string[];
|
||||
};
|
||||
|
||||
export type UserCreate = {
|
||||
|
||||
Reference in New Issue
Block a user