add contract storage fix various bugs and translations
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||||
|
.vscode
|
||||||
### Python ###
|
### Python ###
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## backend\src\contracts\contracts.py
|
||||||
|
|
||||||
|
- Send contract to referer
|
||||||
|
- Extract recap
|
||||||
|
- Extract all contracts
|
||||||
|
- store total price
|
||||||
|
- store pdf file
|
||||||
|
|
||||||
|
## Wording
|
||||||
|
|
||||||
|
- planned -> occasionnal (planifié -> occasionnel)
|
||||||
|
- all translations
|
||||||
|
|
||||||
|
## Tutorial for referers
|
||||||
|
|
||||||
|
### Glossary
|
||||||
|
|
||||||
|
Occasional
|
||||||
|
Recurrent
|
||||||
|
|
||||||
|
## Footer
|
||||||
|
|
||||||
|
### Legal
|
||||||
|
|
||||||
|
### About
|
||||||
|
|
||||||
|
### Contact
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
- use alembic for migration management
|
||||||
|
|
||||||
|
## Update contract after register
|
||||||
|
|
||||||
|
## Filter contracts in home view
|
||||||
|
|
||||||
|
## Only show current season (if multiple form, only show the one with latest start date)
|
||||||
@@ -1,65 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from src.database import get_session
|
from src.database import get_session
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
import src.forms.service as form_service
|
|
||||||
import src.shipments.service as shipment_service
|
|
||||||
import src.products.service as product_service
|
|
||||||
from src.contracts.generate_contract import generate_html_contract
|
from src.contracts.generate_contract import generate_html_contract
|
||||||
import src.models as models
|
import src.models as models
|
||||||
from src.messages import PDFerrorOccured
|
from src.messages import PDFerrorOccured
|
||||||
|
import src.contracts.service as service
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
router = APIRouter(prefix='/contracts')
|
router = APIRouter(prefix='/contracts')
|
||||||
|
|
||||||
def find_dict_in_list(lst, key, value):
|
|
||||||
for i, dic in enumerate(lst):
|
|
||||||
if dic[key].id == value:
|
|
||||||
return i
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def extract_products(session: Session, contract: dict):
|
|
||||||
planned = []
|
|
||||||
recurrent = []
|
|
||||||
for key in contract.keys():
|
|
||||||
key_list = key.split("-")
|
|
||||||
if "planned" in key:
|
|
||||||
shipment_id = int(key_list[1])
|
|
||||||
product_id = int(key_list[2])
|
|
||||||
shipment = shipment_service.get_one(session, shipment_id)
|
|
||||||
product = product_service.get_one(session, product_id)
|
|
||||||
|
|
||||||
existing_id = find_dict_in_list(planned, "shipment", shipment_id)
|
|
||||||
if existing_id >= 0:
|
|
||||||
planned[existing_id]["products"].append({
|
|
||||||
"product": product,
|
|
||||||
"quantity": contract[key],
|
|
||||||
})
|
|
||||||
planned[existing_id]['price'] += compute_product_price(product, contract[key])
|
|
||||||
else:
|
|
||||||
planned.append({
|
|
||||||
"shipment": shipment,
|
|
||||||
"price": compute_product_price(product, contract[key]),
|
|
||||||
"products": [{
|
|
||||||
"product": product,
|
|
||||||
"quantity": contract[key],
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
if "recurrent" in key:
|
|
||||||
product_id = int(key_list[1])
|
|
||||||
product = product_service.get_one(session, product_id)
|
|
||||||
recurrent.append({
|
|
||||||
"product": product,
|
|
||||||
"quantity": contract[key]
|
|
||||||
})
|
|
||||||
return planned, recurrent
|
|
||||||
|
|
||||||
def compute_product_price(product: models.Product, quantity: int, nb_shipment: int = 1):
|
|
||||||
product_quantity_unit = 1 if product.unit == models.Unit.KILO else 1000
|
|
||||||
final_quantity = quantity if product.price else quantity / product_quantity_unit
|
|
||||||
final_price = product.price if product.price else product.price_kg
|
|
||||||
return final_price * final_quantity * nb_shipment
|
|
||||||
|
|
||||||
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
|
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
|
||||||
result = 0
|
result = 0
|
||||||
for product_quantity in products_quantities:
|
for product_quantity in products_quantities:
|
||||||
@@ -68,41 +19,106 @@ def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
|
|||||||
result += compute_product_price(product, quantity, nb_shipment)
|
result += compute_product_price(product, quantity, nb_shipment)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def compute_planned_prices(planned: list[dict]):
|
def compute_planned_prices(planneds: list[dict]):
|
||||||
result = 0
|
result = 0
|
||||||
for plan in planned:
|
for planned in planneds:
|
||||||
result += plan['price']
|
result += planned['price']
|
||||||
|
return result
|
||||||
|
|
||||||
|
def compute_product_price(product: models.Product, quantity: int, nb_shipment: int = 1):
|
||||||
|
product_quantity_unit = 1 if product.unit == models.Unit.KILO else 1000
|
||||||
|
final_quantity = quantity if product.price else quantity / product_quantity_unit
|
||||||
|
final_price = product.price if product.price else product.price_kg
|
||||||
|
return final_price * final_quantity * nb_shipment
|
||||||
|
|
||||||
|
def find_dict_in_list(lst, key, value):
|
||||||
|
for i, dic in enumerate(lst):
|
||||||
|
if dic[key].id == value:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def create_planned_dict(contract_products: list[models.ContractProduct]):
|
||||||
|
result = []
|
||||||
|
for contract_product in contract_products:
|
||||||
|
existing_id = find_dict_in_list(
|
||||||
|
result,
|
||||||
|
'shipment',
|
||||||
|
contract_product.shipment.id
|
||||||
|
)
|
||||||
|
if existing_id < 0:
|
||||||
|
result.append({
|
||||||
|
'shipment': contract_product.shipment,
|
||||||
|
'price': compute_product_price(
|
||||||
|
contract_product.product,
|
||||||
|
contract_product.quantity
|
||||||
|
),
|
||||||
|
'products': [{
|
||||||
|
'product': contract_product.product,
|
||||||
|
'quantity': contract_product.quantity
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
result[existing_id]['products'].append({
|
||||||
|
'product': contract_product.product,
|
||||||
|
'quantity': contract_product.quantity
|
||||||
|
})
|
||||||
|
result[existing_id]['price'] += compute_product_price(
|
||||||
|
contract_product.product,
|
||||||
|
contract_product.quantity
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.post('/')
|
@router.post('/')
|
||||||
async def create_contract(
|
async def create_contract(
|
||||||
contract: models.ContractBase,
|
contract: models.ContractCreate,
|
||||||
session: Session = Depends(get_session)
|
session: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
form = form_service.get_one(session, contract.form_id)
|
new_contract = service.create_one(session, contract)
|
||||||
planned, recurrent = extract_products(session, contract.contract)
|
planned_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.PLANNED, new_contract.products))
|
||||||
recurrent_price = compute_recurrent_prices(recurrent, len(form.shipments))
|
planneds = create_planned_dict(planned_contract_products)
|
||||||
total_price = '{:10.2f}'.format(recurrent_price + compute_planned_prices(planned))
|
recurrents = list(map(lambda x: {"product": x.product, "quantity": x.quantity}, filter(lambda contract_product: contract_product.product.type == models.ProductType.RECCURENT, new_contract.products)))
|
||||||
# TODO: Store contract
|
recurrent_price = compute_recurrent_prices(recurrents, len(new_contract.form.shipments))
|
||||||
|
total_price = '{:10.2f}'.format(recurrent_price + compute_planned_prices(planneds))
|
||||||
|
cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
|
||||||
# TODO: send contract to referer
|
# TODO: send contract to referer
|
||||||
# TODO: Store contract informations ?
|
|
||||||
try:
|
try:
|
||||||
pdf_bytes = generate_html_contract(
|
pdf_bytes = generate_html_contract(
|
||||||
form,
|
new_contract,
|
||||||
contract.contract,
|
cheques,
|
||||||
planned,
|
planneds,
|
||||||
recurrent,
|
recurrents,
|
||||||
'{:10.2f}'.format(recurrent_price),
|
recurrent_price,
|
||||||
total_price
|
total_price
|
||||||
)
|
)
|
||||||
pdf_file = io.BytesIO(pdf_bytes)
|
pdf_file = io.BytesIO(pdf_bytes)
|
||||||
contract_id = f'{contract.contract['firstname']}_{contract.contract['lastname']}_{form.productor.type}_{form.season}'
|
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
|
||||||
except:
|
except:
|
||||||
raise HTTPException(status_code=400, detail=PDFerrorOccured)
|
raise HTTPException(status_code=400, detail=PDFerrorOccured)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
pdf_file,
|
pdf_file,
|
||||||
media_type="application/pdf",
|
media_type='application/pdf',
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": f"attachement; filename=contract_{contract_id}.pdf"
|
'Content-Disposition': f'attachement; filename=contract_{contract_id}.pdf'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.get('/', response_model=list[models.ContractPublic])
|
||||||
|
def get_contracts(
|
||||||
|
forms: list[str] = Query([]),
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
return service.get_all(session, forms)
|
||||||
|
|
||||||
|
@router.get('/{id}', response_model=models.ContractPublic)
|
||||||
|
def get_contract(id: int, 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.delete('/{id}', response_model=models.ContractPublic)
|
||||||
|
def delete_contract(id: int, session: Session = Depends(get_session)):
|
||||||
|
result = service.delete_one(session, id)
|
||||||
|
if result is None:
|
||||||
|
raise HTTPException(status_code=404, detail=messages.notfound)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import html
|
|||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
|
|
||||||
def generate_html_contract(
|
def generate_html_contract(
|
||||||
form: models.Form,
|
contract: models.Contract,
|
||||||
contract_informations: dict,
|
cheques: list[dict],
|
||||||
planned: list[dict],
|
planneds: list[dict],
|
||||||
recurrent: list[dict],
|
reccurents: list[dict],
|
||||||
recurrent_price: float,
|
recurrent_price: float,
|
||||||
total_price: float,
|
total_price: float
|
||||||
):
|
):
|
||||||
template_dir = "./src/contracts/templates"
|
template_dir = "./src/contracts/templates"
|
||||||
template_loader = jinja2.FileSystemLoader(searchpath=template_dir)
|
template_loader = jinja2.FileSystemLoader(searchpath=template_dir)
|
||||||
@@ -18,24 +18,26 @@ def generate_html_contract(
|
|||||||
template_file = "layout.html"
|
template_file = "layout.html"
|
||||||
template = template_env.get_template(template_file)
|
template = template_env.get_template(template_file)
|
||||||
output_text = template.render(
|
output_text = template.render(
|
||||||
contract_name=form.name,
|
contract_name=contract.form.name,
|
||||||
contract_type=form.productor.type,
|
contract_type=contract.form.productor.type,
|
||||||
contract_season=form.season,
|
contract_season=contract.form.season,
|
||||||
referer_name=form.referer.name,
|
referer_name=contract.form.referer.name,
|
||||||
referer_email=form.referer.email,
|
referer_email=contract.form.referer.email,
|
||||||
productor_name=form.productor.name,
|
productor_name=contract.form.productor.name,
|
||||||
productor_address=form.productor.address,
|
productor_address=contract.form.productor.address,
|
||||||
payment_methods_map={"cheque": "Ordre du chèque", "transfer": "IBAN (paiement par virements)"},
|
payment_methods_map={"cheque": "Ordre du chèque", "transfer": "virements"},
|
||||||
productor_payment_methods=form.productor.payment_methods,
|
productor_payment_methods=contract.form.productor.payment_methods,
|
||||||
member_name=f'{html.escape(contract_informations["firstname"])} {html.escape(contract_informations["lastname"])}',
|
member_name=f'{html.escape(contract.firstname)} {html.escape(contract.lastname)}',
|
||||||
member_email=html.escape(contract_informations["email"]),
|
member_email=html.escape(contract.email),
|
||||||
member_phone=html.escape(contract_informations["phone"]),
|
member_phone=html.escape(contract.phone),
|
||||||
contract_start_date=form.start,
|
contract_start_date=contract.form.start,
|
||||||
contract_end_date=form.end,
|
contract_end_date=contract.form.end,
|
||||||
planned=planned,
|
planneds=planneds,
|
||||||
recurrent=recurrent,
|
recurrents=reccurents,
|
||||||
recurrent_price=recurrent_price,
|
recurrent_price=recurrent_price,
|
||||||
total_price=total_price,
|
total_price=total_price,
|
||||||
|
contract_payment_method={"cheque": "chèque", "transfer": "virements"}[contract.payment_method],
|
||||||
|
cheques=cheques
|
||||||
)
|
)
|
||||||
options = {
|
options = {
|
||||||
'page-size': 'Letter',
|
'page-size': 'Letter',
|
||||||
|
|||||||
65
backend/src/contracts/service.py
Normal file
65
backend/src/contracts/service.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from sqlmodel import Session, select
|
||||||
|
import src.models as models
|
||||||
|
|
||||||
|
def get_all(
|
||||||
|
session: Session,
|
||||||
|
forms: list[str]
|
||||||
|
) -> list[models.ContractPublic]:
|
||||||
|
statement = select(models.Contract)
|
||||||
|
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()
|
||||||
|
|
||||||
|
def get_one(session: Session, contract_id: int) -> models.ContractPublic:
|
||||||
|
return session.get(models.Contract, contract_id)
|
||||||
|
|
||||||
|
def create_one(session: Session, contract: models.ContractCreate) -> models.ContractPublic:
|
||||||
|
contract_create = contract.model_dump(exclude_unset=True, exclude=["products", "cheques"])
|
||||||
|
new_contract = models.Contract(**contract_create)
|
||||||
|
|
||||||
|
new_contract.cheques = [
|
||||||
|
models.Cheque(
|
||||||
|
name=cheque.name,
|
||||||
|
value=cheque.value,
|
||||||
|
contract_id=new_contract.id
|
||||||
|
) for cheque in contract.cheques
|
||||||
|
]
|
||||||
|
|
||||||
|
new_contract.products = [
|
||||||
|
models.ContractProduct(
|
||||||
|
contract_id=new_contract.id,
|
||||||
|
product_id=contract_product.product_id,
|
||||||
|
shipment_id=contract_product.shipment_id,
|
||||||
|
quantity=contract_product.quantity
|
||||||
|
) for contract_product in contract.products
|
||||||
|
]
|
||||||
|
|
||||||
|
session.add(new_contract)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(new_contract)
|
||||||
|
return new_contract
|
||||||
|
|
||||||
|
def update_one(session: Session, id: int, contract: models.ContractUpdate) -> models.ContractPublic:
|
||||||
|
statement = select(models.Contract).where(models.Contract.id == id)
|
||||||
|
result = session.exec(statement)
|
||||||
|
new_contract = result.first()
|
||||||
|
if not new_contract:
|
||||||
|
return None
|
||||||
|
contract_updates = contract.model_dump(exclude_unset=True)
|
||||||
|
for key, value in contract_updates.items():
|
||||||
|
setattr(new_contract, key, value)
|
||||||
|
session.add(new_contract)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(new_contract)
|
||||||
|
return new_contract
|
||||||
|
|
||||||
|
def delete_one(session: Session, id: int) -> models.ContractPublic:
|
||||||
|
statement = select(models.Contract).where(models.Contract.id == id)
|
||||||
|
result = session.exec(statement)
|
||||||
|
contract = result.first()
|
||||||
|
if not contract:
|
||||||
|
return None
|
||||||
|
result = models.ContractPublic.model_validate(contract)
|
||||||
|
session.delete(contract)
|
||||||
|
session.commit()
|
||||||
|
return result
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{contract_name}}</title>
|
<title>{{contract_name}}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@page {
|
@page {
|
||||||
size: A4;
|
size: A4;
|
||||||
@@ -23,7 +23,11 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
page-break-after: avoid;
|
page-break-after: avoid;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
@@ -88,7 +92,8 @@
|
|||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -121,15 +126,21 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>AMAP Croix Luizet</h1>
|
<h1>AMAP Croix Luizet</h1>
|
||||||
<h2>67 rue Octavie Villeurbanne - <a href="https://amapcroixluizet.eu">https://amapcroixluizet.eu</a></h2>
|
<h2>
|
||||||
|
67 rue Octavie Villeurbanne -
|
||||||
|
<a href="https://amapcroixluizet.eu">https://amapcroixluizet.eu</a>
|
||||||
|
</h2>
|
||||||
<h3>Contrat d'engagement solidaire</h3>
|
<h3>Contrat d'engagement solidaire</h3>
|
||||||
<h4>Informations contractuelles</h4>
|
<h4>Informations contractuelles</h4>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>Ce contrat est organisé par l’AMAP CROIX-LUIZET et est régi par les statuts et le règlement intérieur de l’Association.</p>
|
<p>
|
||||||
|
Ce contrat est organisé par l’AMAP CROIX-LUIZET et est régi par les
|
||||||
|
statuts et le règlement intérieur de l’Association.
|
||||||
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -160,12 +171,13 @@
|
|||||||
<th>Adresse du producteur·trice</th>
|
<th>Adresse du producteur·trice</th>
|
||||||
<td>{{productor_address}}</td>
|
<td>{{productor_address}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for method in productor_payment_methods %}
|
{% for method in productor_payment_methods %} {% if method.details
|
||||||
|
!= "" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{payment_methods_map[method.name]}}</th>
|
<th>{{payment_methods_map[method.name]}}</th>
|
||||||
<td>{{method.details}}</td>
|
<td>{{method.details}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endif %} {% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>L’adhérent·e</th>
|
<th>L’adhérent·e</th>
|
||||||
<td>{{member_name}}</td>
|
<td>{{member_name}}</td>
|
||||||
@@ -181,15 +193,23 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>
|
<p>
|
||||||
L'adhérent-e et le-la producteur-trice s’engagent à respecter le présent contrat, les statuts et le Règlement Intérieur de «l’AMAP CROIX LUIZET» et la charte des AMAP.
|
L'adhérent-e et le-la producteur-trice s’engagent à respecter le
|
||||||
|
présent contrat, les statuts et le Règlement Intérieur de «l’AMAP
|
||||||
|
CROIX LUIZET» et la charte des AMAP.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5>Engagement réciproque</h5>
|
<h5>Engagement réciproque</h5>
|
||||||
<p>
|
<p>
|
||||||
Le/La producteur·trice s’engage à fournir un panier <b>{{contract_type}}</b>, issu de son exploitation et de qualité en termes gustatifs. Il/Elle s’engage à mener son exploitation dans un esprit de respect de la nature et de l’environnement.
|
Le/La producteur·trice s’engage à fournir un panier
|
||||||
Le/La membre adhérent·e s’engage à acheter 1 panier en acceptant les conséquences d’aléas climatiques ou autres évènements ayant un impact sur la qualité ou la quantité de produits dans le panier.
|
<b>{{contract_type}}</b>, issu de son exploitation et de qualité en
|
||||||
Le contrat commence le <b>{{contract_start_date}}</b> et termine le <b>{{contract_end_date}}</b>.
|
termes gustatifs. Il/Elle s’engage à mener son exploitation dans un
|
||||||
|
esprit de respect de la nature et de l’environnement. Le/La membre
|
||||||
|
adhérent·e s’engage à acheter 1 panier en acceptant les conséquences
|
||||||
|
d’aléas climatiques ou autres évènements ayant un impact sur la
|
||||||
|
qualité ou la quantité de produits dans le panier. Le contrat commence
|
||||||
|
le <b>{{contract_start_date}}</b> et termine le
|
||||||
|
<b>{{contract_end_date}}</b>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -197,29 +217,43 @@
|
|||||||
<b>Modalités de livraison</b>
|
<b>Modalités de livraison</b>
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Les livraisons sont effectuées exclusivement à la Maison du Citoyen, 67 rue Octavie – 69100 VILLEURBANNE, les jeudis soir de 19h00 à 20h00. Toutefois en accord avec le producteur, et suivant les mesures sanitaires en vigueur, le Conseil d’Administration peut modifier exceptionnellement le lieu, le jour ou l’horaire de livraison.
|
Les livraisons sont effectuées exclusivement à la Maison du Citoyen,
|
||||||
|
67 rue Octavie – 69100 VILLEURBANNE, les jeudis soir de 19h00 à 20h00.
|
||||||
|
Toutefois en accord avec le producteur, et suivant les mesures
|
||||||
|
sanitaires en vigueur, le Conseil d’Administration peut modifier
|
||||||
|
exceptionnellement le lieu, le jour ou l’horaire de livraison.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5>
|
<h5>En cas d’impossibilité</h5>
|
||||||
En cas d’impossibilité
|
|
||||||
</h5>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Pour le/la producteur·trice d’assurer une livraison, le Conseil d’Administration et le/la référent-e producteur·trice rechercheront, dans le respect des parties et de l’éthique de l’AMAP une solution compensatrice.
|
Pour le/la producteur·trice d’assurer une livraison, le Conseil
|
||||||
|
d’Administration et le/la référent-e producteur·trice rechercheront,
|
||||||
|
dans le respect des parties et de l’éthique de l’AMAP une solution
|
||||||
|
compensatrice.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Pour l’adhérent·e de respecter le calendrier et de venir récupérer sa commande, les membres chargés de la distribution disposeront des paniers restants qui seront donnés à une association caritative ou distribués aux Amapien·ennes présent·es. Aucun panier ne sera remboursé.
|
Pour l’adhérent·e de respecter le calendrier et de venir récupérer
|
||||||
|
sa commande, les membres chargés de la distribution disposeront des
|
||||||
|
paniers restants qui seront donnés à une association caritative ou
|
||||||
|
distribués aux Amapien·ennes présent·es. Aucun panier ne sera
|
||||||
|
remboursé.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5>Rupture du contrat</h5>
|
<h5>Rupture du contrat</h5>
|
||||||
<p>
|
<p>
|
||||||
Ce contrat peut être interrompu unilatéralement par le/la membre adhérent, si et seulement si, un/une remplaçant·e est trouvé immédiatement, de sorte que le/la producteur·trice ne soit pas pénalisé financièrement. Ce contrat peut être rompu bilatéralement à tout moment. En cas de désaccord, c’est au conseil d’administration de statuer.
|
Ce contrat peut être interrompu unilatéralement par le/la membre
|
||||||
|
adhérent, si et seulement si, un/une remplaçant·e est trouvé
|
||||||
|
immédiatement, de sorte que le/la producteur·trice ne soit pas
|
||||||
|
pénalisé financièrement. Ce contrat peut être rompu bilatéralement à
|
||||||
|
tout moment. En cas de désaccord, c’est au conseil d’administration de
|
||||||
|
statuer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% if recurrent|length > 0 %}
|
{% if recurrents|length > 0 %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h4>Produits récurents (pour chaques livraisons)</h4>
|
<h4>Produits récurents (pour chaques livraisons)</h4>
|
||||||
<table>
|
<table>
|
||||||
@@ -233,13 +267,20 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for rec in recurrent %}
|
{% for rec in recurrents %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{rec.product.name}}</td>
|
<td>{{rec.product.name}}</td>
|
||||||
<td>{{rec.product.price if rec.product.price else ""}}</td>
|
<td>{{rec.product.price if rec.product.price else ""}}</td>
|
||||||
<td>{{rec.product.price_kg if rec.product.price_kg else ""}}</td>
|
<td>{{rec.product.price_kg if rec.product.price_kg else ""}}</td>
|
||||||
<td>{{rec.product.quantity if rec.product.quantity != None else ""}} {{rec.product.quantity_unit if rec.product.quantity_unit != None else ""}}</td>
|
<td>
|
||||||
<td>{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if rec.product.unit == "2" else "p" }}</td>
|
{{rec.product.quantity if rec.product.quantity != None else ""}}
|
||||||
|
{{rec.product.quantity_unit if rec.product.quantity_unit != None
|
||||||
|
else ""}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if
|
||||||
|
rec.product.unit == "2" else "p" }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -249,11 +290,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {% if planneds|length > 0 %}
|
||||||
{% if planned|length > 0 %}
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h4>Produits planifiés (par livraison)</h4>
|
<h4>Produits planifiés (par livraison)</h4>
|
||||||
{% for plan in planned %}
|
{% for plan in planneds %}
|
||||||
<h5>{{plan.shipment.name}} {{plan.shipment.date}}</h5>
|
<h5>{{plan.shipment.name}} {{plan.shipment.date}}</h5>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -269,10 +309,21 @@
|
|||||||
{% for product in plan.products %}
|
{% for product in plan.products %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{product.product.name}}</td>
|
<td>{{product.product.name}}</td>
|
||||||
<td>{{product.product.price if product.product.price else ""}}</td>
|
<td>
|
||||||
<td>{{product.product.price_kg if product.product.price_kg else ""}}</td>
|
{{product.product.price if product.product.price else ""}}
|
||||||
<td>{{product.product.quantity if product.product.quantity != None else ""}} {{product.product.quantity_unit if product.product.quantity_unit != None else ""}}</td>
|
</td>
|
||||||
<td>{{product.quantity}}{{"g" if product.product.unit == "1" else "kg" if product.product.unit == "2" else "p" }}</td>
|
<td>
|
||||||
|
{{product.product.price_kg if product.product.price_kg else ""}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{product.product.quantity if product.product.quantity != None
|
||||||
|
else ""}} {{product.product.quantity_unit if
|
||||||
|
product.product.quantity_unit != None else ""}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{product.quantity}}{{"g" if product.product.unit == "1" else
|
||||||
|
"kg" if product.product.unit == "2" else "p" }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor%}
|
{% endfor%}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -288,6 +339,27 @@
|
|||||||
<div class="total-label">Prix Total :</div>
|
<div class="total-label">Prix Total :</div>
|
||||||
<div class="total-price">{{total_price}}€</div>
|
<div class="total-price">{{total_price}}€</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h4>Paiement par {{contract_payment_method}}</h4>
|
||||||
|
{% if contract_payment_method == "chèque" %}
|
||||||
|
<div class="container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for cheque in cheques %}
|
||||||
|
<th>Cheque n°{{cheque.name}}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{% for cheque in cheques %}
|
||||||
|
<td>{{cheque.value}}€</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -305,5 +377,5 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -140,6 +140,10 @@ class Form(FormBase, table=True):
|
|||||||
"order_by": "Shipment.name"
|
"order_by": "Shipment.name"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
contracts: list["Contract"] = Relationship(
|
||||||
|
back_populates="form",
|
||||||
|
cascade_delete=True
|
||||||
|
)
|
||||||
|
|
||||||
class FormUpdate(SQLModel):
|
class FormUpdate(SQLModel):
|
||||||
name: str | None
|
name: str | None
|
||||||
@@ -168,20 +172,92 @@ class TemplateUpdate(SQLModel):
|
|||||||
class TemplateCreate(TemplateBase):
|
class TemplateCreate(TemplateBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ChequeBase(SQLModel):
|
||||||
|
name: str
|
||||||
|
value: str
|
||||||
|
|
||||||
|
class Cheque(ChequeBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
contract_id: int = Field(foreign_key="contract.id", ondelete="CASCADE")
|
||||||
|
contract: Optional["Contract"] = Relationship(
|
||||||
|
back_populates="cheques",
|
||||||
|
)
|
||||||
|
|
||||||
class ContractBase(SQLModel):
|
class ContractBase(SQLModel):
|
||||||
|
firstname: str
|
||||||
|
lastname: str
|
||||||
|
email: str
|
||||||
|
phone: str
|
||||||
|
payment_method: str
|
||||||
|
cheque_quantity: int
|
||||||
|
|
||||||
|
class Contract(ContractBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
|
||||||
|
form_id: int = Field(
|
||||||
|
foreign_key="form.id",
|
||||||
|
nullable=False,
|
||||||
|
ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
products: list["ContractProduct"] = Relationship(
|
||||||
|
back_populates="contract",
|
||||||
|
cascade_delete=True
|
||||||
|
)
|
||||||
|
form: Optional[Form] = Relationship(back_populates="contracts")
|
||||||
|
cheques: list[Cheque] = Relationship(
|
||||||
|
back_populates="contract",
|
||||||
|
cascade_delete=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class ContractCreate(ContractBase):
|
||||||
|
products: list["ContractProductCreate"] = []
|
||||||
|
cheques: list["Cheque"] = []
|
||||||
form_id: int
|
form_id: int
|
||||||
contract: dict
|
|
||||||
|
|
||||||
class ContractPublic(ContractBase):
|
|
||||||
id: int
|
|
||||||
|
|
||||||
# class Contract(ContractBase, table=True):
|
|
||||||
# id: int | None = Field(default=None, primary_key=True)
|
|
||||||
|
|
||||||
class ContractUpdate(SQLModel):
|
class ContractUpdate(SQLModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ContractCreate(ContractBase):
|
class ContractPublic(ContractBase):
|
||||||
|
id: int
|
||||||
|
products: list["ContractProduct"] = []
|
||||||
|
form: Form
|
||||||
|
|
||||||
|
class ContractProductBase(SQLModel):
|
||||||
|
product_id: int = Field(
|
||||||
|
foreign_key="product.id",
|
||||||
|
nullable=False,
|
||||||
|
ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
shipment_id: int | None = Field(
|
||||||
|
default=None,
|
||||||
|
foreign_key="shipment.id",
|
||||||
|
nullable=True,
|
||||||
|
ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
quantity: float
|
||||||
|
|
||||||
|
class ContractProduct(ContractProductBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
contract_id: int = Field(
|
||||||
|
foreign_key="contract.id",
|
||||||
|
nullable=False,
|
||||||
|
ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
contract: Optional["Contract"] = Relationship(back_populates="products")
|
||||||
|
product: Optional["Product"] = Relationship()
|
||||||
|
shipment: Optional["Shipment"] = Relationship()
|
||||||
|
|
||||||
|
class ContractProductPublic(ContractProductBase):
|
||||||
|
id: int
|
||||||
|
quantity: float
|
||||||
|
contract: Contract
|
||||||
|
product: Product
|
||||||
|
shipment: Optional["Shipment"]
|
||||||
|
|
||||||
|
class ContractProductCreate(ContractProductBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ContractProductUpdate(ContractProductBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ShipmentBase(SQLModel):
|
class ShipmentBase(SQLModel):
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ coverage
|
|||||||
out
|
out
|
||||||
public
|
public
|
||||||
*.lock
|
*.lock
|
||||||
|
*.html
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"product name": "product name",
|
"product name": "name of the product",
|
||||||
"product price": "product price",
|
"product price": "price of the product",
|
||||||
"product quantity": "product quantity",
|
"product quantity": "quantity of the product",
|
||||||
"product quantity unit": "product quantity unit",
|
"product quantity unit": "unit of the product quantity",
|
||||||
"product type": "product type",
|
"product type": "type of the product",
|
||||||
"planned": "planned",
|
"planned": "planned",
|
||||||
"planned products": "planned products",
|
"planned products": "planned products",
|
||||||
"select products per shipment": "select products per shipment.",
|
"select products per shipment": "select products for each shipment",
|
||||||
"recurrent products": "recurrent products",
|
"recurrent products": "recurrent products",
|
||||||
"your selection in this category will apply for all shipments": "your selection in this category will apply for all shipments.",
|
"your selection in this category will apply for all shipments": "selection in this category applies to all shipments",
|
||||||
"recurrent": "recurrent",
|
"recurrent": "recurrent",
|
||||||
"product price kg": "product price kg",
|
"product price kg": "price per kilogram",
|
||||||
"product unit": "product unit",
|
"product unit": "product unit",
|
||||||
"grams": "grams",
|
"grams": "grams",
|
||||||
"kilo": "kilo",
|
"kilo": "kilogram",
|
||||||
"piece": "piece",
|
"piece": "piece",
|
||||||
"filter by season": "filter by season",
|
"filter by season": "filter by season",
|
||||||
"filter by form": "filter by form",
|
"filter by form": "filter by form",
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
"productor": "productor",
|
"productor": "productor",
|
||||||
"referer": "referer",
|
"referer": "referer",
|
||||||
"edit form": "edit form",
|
"edit form": "edit form",
|
||||||
"form name": "form name",
|
"form name": "name of the form",
|
||||||
"contract season": "contract season",
|
"contract season": "contract season",
|
||||||
"contract season recommandation": "recommandation : <Season>-<year> (example: Winter-2025)",
|
"contract season recommandation": "recommendation: <Season>-<year> (example: Winter-2025)",
|
||||||
"start date": "start date",
|
"start date": "start date",
|
||||||
"end date": "end date",
|
"end date": "end date",
|
||||||
"nothing found": "nothing found",
|
"nothing found": "nothing found",
|
||||||
"number of shipment": "number of shipment",
|
"number of shipment": "number of shipments",
|
||||||
"cancel": "cancel",
|
"cancel": "cancel",
|
||||||
"create form": "create form",
|
"create form": "create form",
|
||||||
"edit productor": "edit productor",
|
"edit productor": "edit productor",
|
||||||
@@ -46,32 +46,32 @@
|
|||||||
"transfer": "transfer",
|
"transfer": "transfer",
|
||||||
"type": "type",
|
"type": "type",
|
||||||
"create productor": "create productor",
|
"create productor": "create productor",
|
||||||
"productor name": "productor name",
|
"productor name": "name of the productor",
|
||||||
"productor type": "productor type",
|
"productor type": "type of the productor",
|
||||||
"productor address": "productor address",
|
"productor address": "address of the productor",
|
||||||
"productor payment": "productor payment",
|
"productor payment": "payment method of the productor",
|
||||||
"priceKg": "priceKg",
|
"priceKg": "price per kilogram",
|
||||||
"quantity": "quantity",
|
"quantity": "quantity",
|
||||||
"quantity unit": "quantity unit",
|
"quantity unit": "unit of quantity",
|
||||||
"unit": "sell unit",
|
"unit": "sell unit",
|
||||||
"price": "price",
|
"price": "price",
|
||||||
"create product": "create product",
|
"create product": "create product",
|
||||||
"informations": "informations",
|
"informations": "information",
|
||||||
"remove product": "remove product",
|
"remove product": "remove product",
|
||||||
"edit product": "edit product",
|
"edit product": "edit product",
|
||||||
"shipment name": "shipment name",
|
"shipment name": "shipment name",
|
||||||
"shipments": "shipments",
|
"shipments": "shipments",
|
||||||
"shipment": "shipment",
|
"shipment": "shipment",
|
||||||
"there is": "there is",
|
"there is": "there is",
|
||||||
"for this contract": "for this contact.",
|
"for this contract": "for this contract",
|
||||||
"shipment date": "shipment date",
|
"shipment date": "shipment date",
|
||||||
"shipment products": "shipment products",
|
"shipment products": "shipment products",
|
||||||
"minimum shipment value": "minimum shipment value",
|
"minimum shipment value": "minimum shipment value",
|
||||||
"shipment form": "shipment form",
|
"shipment form": "shipment form",
|
||||||
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "shipment products is necessary only for planned products (if all products are recurrent leave empty)",
|
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "shipment products required only for planned products (leave empty if all products are recurrent)",
|
||||||
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "recurrent product is for all shipments, planned product is for a specific shipment (see shipment form).",
|
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "recurrent products apply to all shipments, planned products apply to a specific shipment (see shipment form)",
|
||||||
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment, ignore this field if it's not the case.",
|
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "ignore this field if minimum shipment value is not required",
|
||||||
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
|
"minimum price for this shipment should be at least": "minimum price for this shipment",
|
||||||
"remove shipment": "remove shipment",
|
"remove shipment": "remove shipment",
|
||||||
"productors": "productors",
|
"productors": "productors",
|
||||||
"products": "products",
|
"products": "products",
|
||||||
@@ -83,6 +83,8 @@
|
|||||||
"actions": "actions",
|
"actions": "actions",
|
||||||
"all productors": "all productors",
|
"all productors": "all productors",
|
||||||
"all products": "all products",
|
"all products": "all products",
|
||||||
|
"all shipments": "all shipments",
|
||||||
|
"all referers": "all referers",
|
||||||
"a name": "a name",
|
"a name": "a name",
|
||||||
"a season": "a season",
|
"a season": "a season",
|
||||||
"a start date": "a start date",
|
"a start date": "a start date",
|
||||||
@@ -90,21 +92,22 @@
|
|||||||
"a productor": "a productor",
|
"a productor": "a productor",
|
||||||
"a referer": "a referer",
|
"a referer": "a referer",
|
||||||
"a phone": "a phone",
|
"a phone": "a phone",
|
||||||
"a fistname": "a fistname",
|
"a fistname": "a firstname",
|
||||||
"a lastname": "a lastname",
|
"a lastname": "a lastname",
|
||||||
"a email": "a email",
|
"a email": "an email",
|
||||||
|
"a payment method": "a payment method",
|
||||||
"submit contract": "submit contract",
|
"submit contract": "submit contract",
|
||||||
"success": "success",
|
"success": "success",
|
||||||
"successfully edited user": "successfully edited user",
|
"successfully edited user": "user edited successfully",
|
||||||
"successfully edited form": "successfully edited form",
|
"successfully edited form": "form edited successfully",
|
||||||
"successfully edited product": "successfully edited product",
|
"successfully edited product": "product edited successfully",
|
||||||
"successfully edited productor": "successfully edited productor",
|
"successfully edited productor": "productor edited successfully",
|
||||||
"successfully edited shipment": "successfully edited shipment",
|
"successfully edited shipment": "shipment edited successfully",
|
||||||
"successfully created user": "successfully created user",
|
"successfully created user": "user created successfully",
|
||||||
"successfully created form": "successfully created form",
|
"successfully created form": "form created successfully",
|
||||||
"successfully created product": "successfully created product",
|
"successfully created product": "product created successfully",
|
||||||
"successfully created productor": "successfully created productor",
|
"successfully created productor": "productor created successfully",
|
||||||
"successfully created shipment": "successfully created shipment",
|
"successfully created shipment": "shipment created successfully",
|
||||||
"error": "error",
|
"error": "error",
|
||||||
"error editing user": "error editing user",
|
"error editing user": "error editing user",
|
||||||
"error editing form": "error editing form",
|
"error editing form": "error editing form",
|
||||||
@@ -121,7 +124,17 @@
|
|||||||
"error deleting product": "error deleting product",
|
"error deleting product": "error deleting product",
|
||||||
"error deleting productor": "error deleting productor",
|
"error deleting productor": "error deleting productor",
|
||||||
"error deleting shipment": "error deleting shipment",
|
"error deleting shipment": "error deleting shipment",
|
||||||
"there is no contract for now": "there is no contract for now.",
|
"there is no contract for now": "no contracts available currently",
|
||||||
"the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form",
|
"for transfer method contact your referer or productor": "for transfer method, contact your referer or productor",
|
||||||
"all theses informations are for contract generation": "all theses informations are for contract generation."
|
"cheque quantity": "number of cheques",
|
||||||
|
"enter cheque quantity": "enter number of cheques",
|
||||||
|
"cheque id": "cheque identifier",
|
||||||
|
"cheque value": "cheque value",
|
||||||
|
"enter cheque value": "enter cheque value",
|
||||||
|
"payment method": "payment method",
|
||||||
|
"enter payment method": "enter payment method",
|
||||||
|
"choose payment method": "choose a payment method (no actual payments, information only)",
|
||||||
|
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "enter 1 to 3 cheques and their identifiers; values are calculated automatically",
|
||||||
|
"the product unit will be assigned to the quantity requested in the form": "product unit matches quantity requested in the form",
|
||||||
|
"all theses informations are for contract generation": "all this information is used for contract generation"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
"product type": "type de produit",
|
"product type": "type de produit",
|
||||||
"planned": "planifié",
|
"planned": "planifié",
|
||||||
"planned products": "Produits planifiés par livraison",
|
"planned products": "Produits planifiés par livraison",
|
||||||
"select products per shipment": "Selectionnez les produits pour chaque livraison.",
|
"select products per shipment": "Sélectionnez les produits pour chaque livraison.",
|
||||||
"recurrent": "récurent",
|
"recurrent": "récurent",
|
||||||
"recurrent products": "Produits récurents",
|
"recurrent products": "Produits récurrents",
|
||||||
"your selection in this category will apply for all shipments": "votre selection sera appliquée pour chaque livraisons (Exemple: Pour 6 livraisons, le produits sera comptés 6 fois : une fois par livraison).",
|
"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és 6 fois : une fois par livraison).",
|
||||||
"product price kg": "prix du produit au Kilo",
|
"product price kg": "prix du produit au Kilo",
|
||||||
"product unit": "unité de vente du produit",
|
"product unit": "unité de vente du produit",
|
||||||
"piece": "pièce",
|
"piece": "pièce",
|
||||||
"in": "en",
|
"in": "en",
|
||||||
"enter quantity": "entrez la quantitée",
|
"enter quantity": "entrez la quantité",
|
||||||
"filter by season": "filtrer par saisons",
|
"filter by season": "filtrer par saisons",
|
||||||
"filter by form": "filtrer par formulaire",
|
"filter by form": "filtrer par formulaire",
|
||||||
"filter by productor": "filtrer par producteur·trice",
|
"filter by productor": "filtrer par producteur·trice",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"nothing found": "rien à afficher",
|
"nothing found": "rien à afficher",
|
||||||
"number of shipment": "nombre de livraisons",
|
"number of shipment": "nombre de livraisons",
|
||||||
"cancel": "annuler",
|
"cancel": "annuler",
|
||||||
"create form": "créer un formulare de contrat",
|
"create form": "créer un formulaire de contrat",
|
||||||
"create productor": "créer le/la producteur·trice",
|
"create productor": "créer le/la producteur·trice",
|
||||||
"edit productor": "modifier le/la producteur·trice",
|
"edit productor": "modifier le/la producteur·trice",
|
||||||
"remove productor": "supprimer le/la producteur·trice",
|
"remove productor": "supprimer le/la producteur·trice",
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
"shipment products": "produits pour la livraison",
|
"shipment products": "produits pour la livraison",
|
||||||
"shipment form": "formulaire lié a la livraison",
|
"shipment form": "formulaire lié a la livraison",
|
||||||
"minimum shipment value": "valeur minimum d'une livraison (€)",
|
"minimum shipment value": "valeur minimum d'une livraison (€)",
|
||||||
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "il est nécéssaire de configurer les produits pour la livraison uniquement si il y a des produits planifiés (laisser vide si tous les produits sont récurents).",
|
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "il est nécessaire de configurer les produits pour la livraison uniquement si il y a des produits planifiés (laisser vide si tous les produits sont récurents).",
|
||||||
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "les produits récurents sont pour toutes les livraisons, les produits planifiés sont pour une livraison particulière (voir formulaire de création de livraison).",
|
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "les produits récurrents sont pour toutes les livraisons, les produits planifiés sont pour une livraison particulière (voir formulaire de création de livraison).",
|
||||||
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré s’il ne s’applique pas à votre contrat.",
|
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré s’il ne s’applique pas à votre contrat.",
|
||||||
"minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de",
|
"minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de",
|
||||||
"there is": "il y a",
|
"there is": "il y a",
|
||||||
@@ -85,6 +85,8 @@
|
|||||||
"actions": "actions",
|
"actions": "actions",
|
||||||
"all productors": "tous les producteur·trices",
|
"all productors": "tous les producteur·trices",
|
||||||
"all products": "tous les produits",
|
"all products": "tous les produits",
|
||||||
|
"all shipments": "toutes les livraisons",
|
||||||
|
"all referers": "tous les référent·es",
|
||||||
"is required": "est requis·e",
|
"is required": "est requis·e",
|
||||||
"a name": "un nom",
|
"a name": "un nom",
|
||||||
"a season": "une saison",
|
"a season": "une saison",
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
"a fistname": "un prénom",
|
"a fistname": "un prénom",
|
||||||
"a lastname": "un nom",
|
"a lastname": "un nom",
|
||||||
"a email": "une adresse email",
|
"a email": "une adresse email",
|
||||||
|
"a payment method": "une méthode de paiement",
|
||||||
"submit contract": "Envoyer le contrat",
|
"submit contract": "Envoyer le contrat",
|
||||||
"mililiter": "mililitres (ml)",
|
"mililiter": "mililitres (ml)",
|
||||||
"grams": "grammes (g)",
|
"grams": "grammes (g)",
|
||||||
@@ -106,17 +109,17 @@
|
|||||||
"successfully edited form": "formulaire correctement édité",
|
"successfully edited form": "formulaire correctement édité",
|
||||||
"successfully edited product": "produit correctement édité",
|
"successfully edited product": "produit correctement édité",
|
||||||
"successfully edited productor": "producteur·trice correctement édité(e)",
|
"successfully edited productor": "producteur·trice correctement édité(e)",
|
||||||
"successfully edited shipment": "livaison correctement éditée",
|
"successfully edited shipment": "livraison correctement éditée",
|
||||||
"successfully created user": "utilisateur·trice correctement créé(e)",
|
"successfully created user": "utilisateur·trice correctement créé(e)",
|
||||||
"successfully created form": "formulaire correctement créé",
|
"successfully created form": "formulaire correctement créé",
|
||||||
"successfully created product": "produit correctement créé",
|
"successfully created product": "produit correctement créé",
|
||||||
"successfully created productor": "producteur·trice correctement créé(e)",
|
"successfully created productor": "producteur·trice correctement créé(e)",
|
||||||
"successfully created shipment": "livaison correctement créée",
|
"successfully created shipment": "livraison correctement créée",
|
||||||
"successfully deleted user": "utilisateur·trice correctement supprimé",
|
"successfully deleted user": "utilisateur·trice correctement supprimé",
|
||||||
"successfully deleted form": "formulaire correctement supprimé",
|
"successfully deleted form": "formulaire correctement supprimé",
|
||||||
"successfully deleted product": "produit correctement supprimé",
|
"successfully deleted product": "produit correctement supprimé",
|
||||||
"successfully deleted productor": "producteur·trice correctement supprimé(e)",
|
"successfully deleted productor": "producteur·trice correctement supprimé(e)",
|
||||||
"successfully deleted shipment": "livaison correctement supprimée",
|
"successfully deleted shipment": "livraison correctement supprimée",
|
||||||
"error": "erreur",
|
"error": "erreur",
|
||||||
"error editing user": "erreur pendant l'édition de l'utilisateur·trice",
|
"error editing user": "erreur pendant l'édition de l'utilisateur·trice",
|
||||||
"error editing form": "erreur pendant l'édition du formulaire",
|
"error editing form": "erreur pendant l'édition du formulaire",
|
||||||
@@ -134,6 +137,16 @@
|
|||||||
"error deleting productor": "erreur pendant la suppression du producteur·trice",
|
"error deleting productor": "erreur pendant la suppression du producteur·trice",
|
||||||
"error deleting shipment": "erreur pendant la suppression de la livraison",
|
"error deleting shipment": "erreur pendant la suppression de la livraison",
|
||||||
"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.",
|
||||||
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.",
|
"for transfer method contact your referer or productor": "pour mettre en place le virement automatique, contactez votre référent ou le producteur.",
|
||||||
"all theses informations are for contract generation": "ces informations sont nécéssaires pour la génération de contrat."
|
"cheque quantity": "quantité de chèques (pour le paiement en plusieurs fois)",
|
||||||
|
"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",
|
||||||
|
"enter payment method": "sélectionnez votre méthode de paiement",
|
||||||
|
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "nombre de chèques entre 1 et 3, entrez également les identifiants des chèques utilisés.",
|
||||||
|
"payment method": "méthode de paiement",
|
||||||
|
"choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).",
|
||||||
|
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.",
|
||||||
|
"all theses informations are for contract generation": "ces informations sont nécessaires pour la génération de contrat."
|
||||||
}
|
}
|
||||||
|
|||||||
30
frontend/src/components/Contracts/Filter/index.tsx
Normal file
30
frontend/src/components/Contracts/Filter/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Group, MultiSelect } from "@mantine/core";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { t } from "@/config/i18n";
|
||||||
|
|
||||||
|
export type ContractFiltersProps = {
|
||||||
|
forms: string[];
|
||||||
|
filters: URLSearchParams;
|
||||||
|
onFilterChange: (values: string[], filter: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContractFilters({ forms, filters, onFilterChange }: ContractFiltersProps) {
|
||||||
|
const defaultNames = useMemo(() => {
|
||||||
|
return filters.getAll("forms");
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<MultiSelect
|
||||||
|
aria-label={t("filter by form", { capfirst: true })}
|
||||||
|
placeholder={t("filter by form", { capfirst: true })}
|
||||||
|
data={forms}
|
||||||
|
defaultValue={defaultNames}
|
||||||
|
onChange={(values: string[]) => {
|
||||||
|
onFilterChange(values, "forms");
|
||||||
|
}}
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
frontend/src/components/Contracts/Modal/index.tsx
Normal file
84
frontend/src/components/Contracts/Modal/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Button, Group, Modal, 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 Contract, type ContractInputs } from "@/services/resources/contracts";
|
||||||
|
|
||||||
|
export type ContractModalProps = ModalBaseProps & {
|
||||||
|
currentContract?: Contract;
|
||||||
|
handleSubmit: (contract: ContractInputs, 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,
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
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"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps("name")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t("contract email", { capfirst: true })}
|
||||||
|
placeholder={t("contract email", { capfirst: true })}
|
||||||
|
radius="sm"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps("email")}
|
||||||
|
/>
|
||||||
|
<Group mt="sm" justify="space-between">
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
aria-label={t("cancel", { capfirst: true })}
|
||||||
|
leftSection={<IconCancel />}
|
||||||
|
onClick={() => {
|
||||||
|
form.clearErrors();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("cancel", { capfirst: true })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
aria-label={
|
||||||
|
currentContract
|
||||||
|
? t("edit contract", { capfirst: true })
|
||||||
|
: t("create contract", { capfirst: true })
|
||||||
|
}
|
||||||
|
leftSection={currentContract ? <IconEdit /> : <IconPlus />}
|
||||||
|
onClick={() => {
|
||||||
|
form.validate();
|
||||||
|
if (form.isValid()) {
|
||||||
|
handleSubmit(form.getValues(), currentContract?.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentContract
|
||||||
|
? t("edit contract", { capfirst: true })
|
||||||
|
: t("create contract", { capfirst: true })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
frontend/src/components/Contracts/Row/index.tsx
Normal file
55
frontend/src/components/Contracts/Row/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ActionIcon, Table, Tooltip } from "@mantine/core";
|
||||||
|
import { type Contract } from "@/services/resources/contracts";
|
||||||
|
import { IconX } from "@tabler/icons-react";
|
||||||
|
import { t } from "@/config/i18n";
|
||||||
|
import { useDeleteContract } from "@/services/api";
|
||||||
|
|
||||||
|
export type ContractRowProps = {
|
||||||
|
contract: Contract;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContractRow({ contract }: ContractRowProps) {
|
||||||
|
// const [searchParams] = useSearchParams();
|
||||||
|
const deleteMutation = useDeleteContract();
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Tr key={contract.id}>
|
||||||
|
<Table.Td>
|
||||||
|
{contract.firstname} {contract.lastname}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{contract.email}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{/* <Tooltip label={t("edit contract", { capfirst: true })}>
|
||||||
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
mr="5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(
|
||||||
|
`/dashboard/contracts/${contract.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip> */}
|
||||||
|
<Tooltip label={t("remove contract", { capfirst: true })}>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
mr="5"
|
||||||
|
onClick={() => {
|
||||||
|
deleteMutation.mutate(contract.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
frontend/src/components/PaymentMethods/Cheque/index.tsx
Normal file
78
frontend/src/components/PaymentMethods/Cheque/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { t } from "@/config/i18n";
|
||||||
|
import type { ContractInputs } from "@/services/resources/contracts";
|
||||||
|
import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core";
|
||||||
|
import type { UseFormReturnType } from "@mantine/form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export type ContractChequeProps = {
|
||||||
|
inputForm: UseFormReturnType<ContractInputs>;
|
||||||
|
price: number;
|
||||||
|
chequeOrder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Cheque = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ContractCheque({ inputForm, price, chequeOrder }: ContractChequeProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inputForm.values.payment_method.includes("cheque")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quantity = Number(inputForm.values.cheque_quantity);
|
||||||
|
if (!quantity || quantity <= 0) return;
|
||||||
|
const cheques = inputForm.values.cheques || [];
|
||||||
|
if (cheques.length !== quantity) {
|
||||||
|
const newCheques = Array.from({ length: quantity }, (_, i) => ({
|
||||||
|
name: cheques[i]?.name ?? "",
|
||||||
|
value: cheques[i]?.value ?? 0,
|
||||||
|
}));
|
||||||
|
inputForm.setFieldValue("cheques", newCheques);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCents = Math.round(price * 100);
|
||||||
|
const base = Math.floor(totalCents / quantity);
|
||||||
|
const rest = totalCents - base * quantity;
|
||||||
|
for (let i = 0; i < quantity; i++) {
|
||||||
|
const val = (i === quantity - 1 ? base + rest : base) / 100;
|
||||||
|
inputForm.setFieldValue(`cheques.${i}.value`, val.toFixed(2));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [inputForm.values.cheque_quantity, price, inputForm.values.cheques]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={4}>{`${t("order name")} : ${chequeOrder}`}</Title>
|
||||||
|
<NumberInput
|
||||||
|
label={t("cheque quantity", { capfirst: true })}
|
||||||
|
placeholder={t("enter cheque quantity", { capfirst: true })}
|
||||||
|
description={t(
|
||||||
|
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically",
|
||||||
|
{ capfirst: true },
|
||||||
|
)}
|
||||||
|
min={1}
|
||||||
|
max={3}
|
||||||
|
{...inputForm.getInputProps(`cheque_quantity`)}
|
||||||
|
/>
|
||||||
|
<Group grow>
|
||||||
|
{inputForm.values.cheques.map((_cheque, index) => (
|
||||||
|
<Stack key={`${index}`}>
|
||||||
|
<TextInput
|
||||||
|
label={t("cheque id", { capfirst: true })}
|
||||||
|
placeholder={t("cheque id", { capfirst: true })}
|
||||||
|
{...inputForm.getInputProps(`cheques.${index}.name`)}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
readOnly
|
||||||
|
label={t("cheque value", { capfirst: true })}
|
||||||
|
suffix={"€"}
|
||||||
|
placeholder={t("enter cheque value", { capfirst: true })}
|
||||||
|
{...inputForm.getInputProps(`cheques.${index}.value`)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { IconCancel } from "@tabler/icons-react";
|
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
PaymentMethods,
|
PaymentMethods,
|
||||||
type Productor,
|
type Productor,
|
||||||
@@ -94,26 +94,16 @@ export function ProductorModal({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{form.values.payment_methods.map((method, index) => (
|
{form.values.payment_methods.map((method, index) =>
|
||||||
|
method.name === "cheque" ? (
|
||||||
<TextInput
|
<TextInput
|
||||||
key={index}
|
key={index}
|
||||||
label={
|
label={t("order name", { capfirst: true })}
|
||||||
method.name === "cheque"
|
placeholder={t("order name", { capfirst: true })}
|
||||||
? t("order name", { capfirst: true })
|
|
||||||
: method.name === "transfer"
|
|
||||||
? t("IBAN")
|
|
||||||
: t("details", { capfirst: true })
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
method.name === "cheque"
|
|
||||||
? t("order name", { capfirst: true })
|
|
||||||
: method.name === "transfer"
|
|
||||||
? t("IBAN")
|
|
||||||
: t("details", { capfirst: true })
|
|
||||||
}
|
|
||||||
{...form.getInputProps(`payment_methods.${index}.details`)}
|
{...form.getInputProps(`payment_methods.${index}.details`)}
|
||||||
/>
|
/>
|
||||||
))}
|
) : null,
|
||||||
|
)}
|
||||||
<Group mt="sm" justify="space-between">
|
<Group mt="sm" justify="space-between">
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@@ -134,6 +124,7 @@ export function ProductorModal({
|
|||||||
? t("edit productor", { capfirst: true })
|
? t("edit productor", { capfirst: true })
|
||||||
: t("create productor", { capfirst: true })
|
: t("create productor", { capfirst: true })
|
||||||
}
|
}
|
||||||
|
leftSection={currentProductor ? <IconEdit /> : <IconPlus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
|
import type { ContractInputs } from "@/services/resources/contracts";
|
||||||
import { ProductUnit, type Product } from "@/services/resources/products";
|
import { ProductUnit, type Product } from "@/services/resources/products";
|
||||||
import type { Shipment } from "@/services/resources/shipments";
|
import type { Shipment } from "@/services/resources/shipments";
|
||||||
import { Group, NumberInput } from "@mantine/core";
|
import { Group, NumberInput } from "@mantine/core";
|
||||||
import type { UseFormReturnType } from "@mantine/form";
|
import type { UseFormReturnType } from "@mantine/form";
|
||||||
|
|
||||||
export type ProductFormProps = {
|
export type ProductFormProps = {
|
||||||
inputForm: UseFormReturnType<Record<string, string | number>>;
|
inputForm: UseFormReturnType<ContractInputs>;
|
||||||
product: Product;
|
product: Product;
|
||||||
shipment?: Shipment;
|
shipment?: Shipment;
|
||||||
};
|
};
|
||||||
@@ -33,7 +34,9 @@ export function ProductForm({ inputForm, product, shipment }: ProductFormProps)
|
|||||||
aria-label={t("enter quantity")}
|
aria-label={t("enter quantity")}
|
||||||
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t(ProductUnit[product.unit])}`}
|
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t(ProductUnit[product.unit])}`}
|
||||||
{...inputForm.getInputProps(
|
{...inputForm.getInputProps(
|
||||||
shipment ? `planned-${shipment.id}-${product.id}` : `recurrent-${product.id}`,
|
shipment
|
||||||
|
? `products.planned-${shipment.id}-${product.id}`
|
||||||
|
: `products.recurrent-${product.id}`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { IconCancel } from "@tabler/icons-react";
|
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
ProductQuantityUnit,
|
ProductQuantityUnit,
|
||||||
ProductUnit,
|
ProductUnit,
|
||||||
@@ -179,6 +179,7 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
|
|||||||
? t("edit product", { capfirst: true })
|
? t("edit product", { capfirst: true })
|
||||||
: t("create product", { capfirst: true })
|
: t("create product", { capfirst: true })
|
||||||
}
|
}
|
||||||
|
leftSection={currentProduct ? <IconEdit /> : <IconPlus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import type { UseFormReturnType } from "@mantine/form";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { computePrices } from "@/pages/Contract/price";
|
import { computePrices } from "@/pages/Contract/price";
|
||||||
|
import type { ContractInputs } from "@/services/resources/contracts";
|
||||||
|
|
||||||
export type ShipmentFormProps = {
|
export type ShipmentFormProps = {
|
||||||
inputForm: UseFormReturnType<Record<string, string | number>>;
|
inputForm: UseFormReturnType<ContractInputs>;
|
||||||
shipment: Shipment;
|
shipment: Shipment;
|
||||||
minimumPrice?: number | null;
|
minimumPrice?: number | null;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -20,7 +21,7 @@ export default function ShipmentForm({
|
|||||||
minimumPrice,
|
minimumPrice,
|
||||||
}: ShipmentFormProps) {
|
}: ShipmentFormProps) {
|
||||||
const shipmentPrice = useMemo(() => {
|
const shipmentPrice = useMemo(() => {
|
||||||
const values = Object.entries(inputForm.getValues()).filter(
|
const values = Object.entries(inputForm.getValues().products).filter(
|
||||||
([key]) => key.includes("planned") && key.split("-")[1] === String(shipment.id),
|
([key]) => key.includes("planned") && key.split("-")[1] === String(shipment.id),
|
||||||
);
|
);
|
||||||
return computePrices(values, shipment.products);
|
return computePrices(values, shipment.products);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { DatePickerInput } from "@mantine/dates";
|
import { DatePickerInput } from "@mantine/dates";
|
||||||
import { IconCancel } from "@tabler/icons-react";
|
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
|
import { type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
|
||||||
@@ -129,6 +129,7 @@ export default function ShipmentModal({
|
|||||||
? t("edit shipment", { capfirst: true })
|
? t("edit shipment", { capfirst: true })
|
||||||
: t("create shipment", { capfirst: true })
|
: t("create shipment", { capfirst: true })
|
||||||
}
|
}
|
||||||
|
leftSection={currentShipment ? <IconEdit /> : <IconPlus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { IconCancel } from "@tabler/icons-react";
|
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||||
import { type User, type UserInputs } from "@/services/resources/users";
|
import { type User, type UserInputs } from "@/services/resources/users";
|
||||||
|
|
||||||
export type UserModalProps = ModalBaseProps & {
|
export type UserModalProps = ModalBaseProps & {
|
||||||
@@ -60,6 +60,7 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
|
|||||||
? t("edit user", { capfirst: true })
|
? t("edit user", { capfirst: true })
|
||||||
: t("create user", { capfirst: true })
|
: t("create user", { capfirst: true })
|
||||||
}
|
}
|
||||||
|
leftSection={currentUser ? <IconEdit /> : <IconPlus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { RouterProvider } from "react-router";
|
|||||||
import { router } from "@/router.tsx";
|
import { router } from "@/router.tsx";
|
||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/dates/styles.css";
|
import "@mantine/dates/styles.css";
|
||||||
import "@mantine/notifications/styles.css";
|
import "@mantine/notifications/styles.css";
|
||||||
import { Notifications } from "@mantine/notifications";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
List,
|
List,
|
||||||
Loader,
|
Loader,
|
||||||
Overlay,
|
Overlay,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -20,16 +22,22 @@ import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
|
|||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { computePrices } from "./price";
|
import { computePrices } from "./price";
|
||||||
|
import { ContractCheque } from "@/components/PaymentMethods/Cheque";
|
||||||
|
import { tranformProducts, type ContractInputs } from "@/services/resources/contracts";
|
||||||
|
|
||||||
export function Contract() {
|
export function Contract() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { data: form } = useGetForm(Number(id), { enabled: !!id });
|
const { data: form } = useGetForm(Number(id), { enabled: !!id });
|
||||||
const inputForm = useForm<Record<string, number | string>>({
|
const inputForm = useForm<ContractInputs>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
firstname: "",
|
firstname: "",
|
||||||
lastname: "",
|
lastname: "",
|
||||||
email: "",
|
email: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
|
payment_method: "",
|
||||||
|
cheque_quantity: 1,
|
||||||
|
cheques: [],
|
||||||
|
products: {},
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
firstname: (value) =>
|
firstname: (value) =>
|
||||||
@@ -40,6 +48,8 @@ export function Contract() {
|
|||||||
!value ? `${t("a email", { capfirst: true })} ${t("is required")}` : null,
|
!value ? `${t("a email", { capfirst: true })} ${t("is required")}` : null,
|
||||||
phone: (value) =>
|
phone: (value) =>
|
||||||
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
|
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
|
||||||
|
payment_method: (value) =>
|
||||||
|
!value ? `${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,8 +71,8 @@ export function Contract() {
|
|||||||
if (!allProducts) {
|
if (!allProducts) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const values = Object.entries(inputForm.getValues());
|
const productValues = Object.entries(inputForm.getValues().products);
|
||||||
return computePrices(values, allProducts, form?.shipments.length);
|
return computePrices(productValues, allProducts, form?.shipments.length);
|
||||||
}, [inputForm, allProducts, form?.shipments]);
|
}, [inputForm, allProducts, form?.shipments]);
|
||||||
|
|
||||||
const inputRefs = useRef<Record<string, HTMLInputElement | null>>({
|
const inputRefs = useRef<Record<string, HTMLInputElement | null>>({
|
||||||
@@ -70,6 +80,7 @@ export function Contract() {
|
|||||||
lastname: null,
|
lastname: null,
|
||||||
email: null,
|
email: null,
|
||||||
phone: null,
|
phone: null,
|
||||||
|
payment_method: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isShipmentsMinimumValue = useCallback(() => {
|
const isShipmentsMinimumValue = useCallback(() => {
|
||||||
@@ -77,7 +88,7 @@ export function Contract() {
|
|||||||
const shipmentErrors = form.shipments
|
const shipmentErrors = form.shipments
|
||||||
.map((shipment) => {
|
.map((shipment) => {
|
||||||
const total = computePrices(
|
const total = computePrices(
|
||||||
Object.entries(inputForm.getValues()),
|
Object.entries(inputForm.getValues().products),
|
||||||
shipment.products,
|
shipment.products,
|
||||||
);
|
);
|
||||||
if (total < (form?.minimum_shipment_value || 0)) {
|
if (total < (form?.minimum_shipment_value || 0)) {
|
||||||
@@ -122,8 +133,9 @@ export function Contract() {
|
|||||||
}
|
}
|
||||||
if (inputForm.isValid() && isShipmentsMinimumValue()) {
|
if (inputForm.isValid() && isShipmentsMinimumValue()) {
|
||||||
const contract = {
|
const contract = {
|
||||||
|
...inputForm.getValues(),
|
||||||
form_id: form.id,
|
form_id: form.id,
|
||||||
contract: withDefaultValues(inputForm.getValues()),
|
products: tranformProducts(withDefaultValues(inputForm.getValues().products)),
|
||||||
};
|
};
|
||||||
await createContractMutation.mutateAsync(contract);
|
await createContractMutation.mutateAsync(contract);
|
||||||
} else {
|
} else {
|
||||||
@@ -253,6 +265,36 @@ export function Contract() {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Title order={3}>{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={form.productor.payment_methods.map((payment) => ({
|
||||||
|
value: payment.name,
|
||||||
|
label: t(payment.name, { capfirst: true }),
|
||||||
|
}))}
|
||||||
|
{...inputForm.getInputProps("payment_method")}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRefs.current.payment_method = el;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{inputForm.values.payment_method === "cheque" ? (
|
||||||
|
<ContractCheque
|
||||||
|
chequeOrder={
|
||||||
|
form?.productor?.payment_methods.find((el) => el.name === "cheque")
|
||||||
|
?.details || ""
|
||||||
|
}
|
||||||
|
price={price}
|
||||||
|
inputForm={inputForm}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{inputForm.values.payment_method === "transfer" ? (
|
||||||
|
<Text>
|
||||||
|
{t("for transfer method contact your referer or productor", { capfirst: true })}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Space h="15vh"></Space>
|
||||||
<Overlay
|
<Overlay
|
||||||
bg={"lightGray"}
|
bg={"lightGray"}
|
||||||
h="10vh"
|
h="10vh"
|
||||||
|
|||||||
120
frontend/src/pages/Contracts/index.tsx
Normal file
120
frontend/src/pages/Contracts/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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 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 ContractsFilters from "@/components/Contracts/Filter";
|
||||||
|
|
||||||
|
export default function Contracts() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
// const location = useLocation();
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 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 { data: contracts, isPending } = useGetContracts(searchParams);
|
||||||
|
// const { data: currentContract } = useGetContract(Number(editId), {
|
||||||
|
// enabled: !!editId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const { data: allContracts } = useGetContracts();
|
||||||
|
|
||||||
|
const forms = useMemo(() => {
|
||||||
|
return allContracts
|
||||||
|
?.map((contract: Contract) => contract.form.name)
|
||||||
|
.filter((contract, index, array) => array.indexOf(contract) === index);
|
||||||
|
}, [allContracts]);
|
||||||
|
|
||||||
|
const onFilterChange = useCallback(
|
||||||
|
(values: string[], filter: string) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const params = new URLSearchParams(prev);
|
||||||
|
params.delete(filter);
|
||||||
|
|
||||||
|
values.forEach((value) => {
|
||||||
|
params.append(filter, value);
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!contracts || isPending)
|
||||||
|
return (
|
||||||
|
<Group align="center" justify="center" h="80vh" w="100%">
|
||||||
|
<Loader color="pink" />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
|
||||||
|
{/* <Tooltip label={t("create contract", { capfirst: true })}>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(
|
||||||
|
`/dashboard/contracts/create${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<ContractModal
|
||||||
|
key={`${currentContract?.id}_create`}
|
||||||
|
opened={isCreate}
|
||||||
|
onClose={closeModal}
|
||||||
|
handleSubmit={handleCreateContract}
|
||||||
|
/>
|
||||||
|
<ContractModal
|
||||||
|
key={`${currentContract?.id}_edit`}
|
||||||
|
opened={isEdit}
|
||||||
|
onClose={closeModal}
|
||||||
|
currentContract={currentContract}
|
||||||
|
handleSubmit={handleEditContract}
|
||||||
|
/> */}
|
||||||
|
</Group>
|
||||||
|
<ContractsFilters
|
||||||
|
forms={forms || []}
|
||||||
|
filters={searchParams}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
/>
|
||||||
|
<ScrollArea type="auto">
|
||||||
|
<Table striped>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>{t("name", { capfirst: true })}</Table.Th>
|
||||||
|
<Table.Th>{t("email", { capfirst: true })}</Table.Th>
|
||||||
|
<Table.Th>{t("payment method", { capfirst: true })}</Table.Th>
|
||||||
|
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{contracts.map((contract) => (
|
||||||
|
<ContractRow contract={contract} key={contract.id} />
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export default function Dashboard() {
|
|||||||
<Tabs.Tab value="products">{t("products", { capfirst: true })}</Tabs.Tab>
|
<Tabs.Tab value="products">{t("products", { capfirst: true })}</Tabs.Tab>
|
||||||
<Tabs.Tab value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
|
<Tabs.Tab value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
|
||||||
<Tabs.Tab value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
|
<Tabs.Tab value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
|
||||||
{/* <Tabs.Tab value="templates">{t("templates", {capfirst: true})}</Tabs.Tab> */}
|
<Tabs.Tab value="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
|
||||||
<Tabs.Tab value="users">{t("users", { capfirst: true })}</Tabs.Tab>
|
<Tabs.Tab value="users">{t("users", { capfirst: true })}</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function Users() {
|
|||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={2}>{t("all users", { capfirst: true })}</Title>
|
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
|
||||||
<Tooltip label={t("create user", { capfirst: true })}>
|
<Tooltip label={t("create user", { capfirst: true })}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Users from "@/pages/Users";
|
|||||||
import Shipments from "./pages/Shipments";
|
import Shipments from "./pages/Shipments";
|
||||||
import { Contract } from "./pages/Contract";
|
import { Contract } from "./pages/Contract";
|
||||||
import { NotFound } from "./pages/NotFound";
|
import { NotFound } from "./pages/NotFound";
|
||||||
|
import Contracts from "./pages/Contracts";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -29,7 +30,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: "products", Component: Products },
|
{ path: "products", Component: Products },
|
||||||
{ path: "products/create", Component: Products },
|
{ path: "products/create", Component: Products },
|
||||||
{ path: "products/:id/edit", Component: Products },
|
{ path: "products/:id/edit", Component: Products },
|
||||||
// { path: "templates", Component: Templates },
|
{ path: "contracts", Component: Contracts },
|
||||||
{ path: "users", Component: Users },
|
{ path: "users", Component: Users },
|
||||||
{ path: "users/create", Component: Users },
|
{ path: "users/create", Component: Users },
|
||||||
{ path: "users/:id/edit", Component: Users },
|
{ path: "users/:id/edit", Component: Users },
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
} from "@/services/resources/productors";
|
} from "@/services/resources/productors";
|
||||||
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
|
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
|
||||||
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
|
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
|
||||||
import type { ContractCreate } from "./resources/contracts";
|
import type { Contract, ContractCreate } from "./resources/contracts";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
|
|
||||||
@@ -563,6 +563,29 @@ export function useEditUser() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contract[], Error> {
|
||||||
|
const queryString = filters?.toString();
|
||||||
|
return useQuery<Contract[]>({
|
||||||
|
queryKey: ["contracts", queryString],
|
||||||
|
queryFn: () =>
|
||||||
|
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`).then(
|
||||||
|
(res) => res.json(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetContract(
|
||||||
|
id?: number,
|
||||||
|
options?: Partial<DefinedInitialDataOptions<Contract, Error, Contract, readonly unknown[]>>,
|
||||||
|
) {
|
||||||
|
return useQuery<Contract>({
|
||||||
|
queryKey: ["contract"],
|
||||||
|
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}`).then((res) => res.json()),
|
||||||
|
enabled: !!id,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useCreateContract() {
|
export function useCreateContract() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -587,3 +610,31 @@ export function useCreateContract() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeleteContract() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => {
|
||||||
|
return fetch(`${Config.backend_uri}/contracts/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}).then((res) => res.json());
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", { capfirst: true }),
|
||||||
|
message: t("successfully deleted contract", { capfirst: true }),
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", { capfirst: true }),
|
||||||
|
message: error?.message || t(`error deleting contract`, { capfirst: true }),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,72 @@
|
|||||||
|
import type { Cheque } from "@/components/PaymentMethods/Cheque";
|
||||||
|
import type { Form } from "./forms";
|
||||||
|
import type { Product } from "./products";
|
||||||
|
import type { Shipment } from "./shipments";
|
||||||
|
|
||||||
|
export type Contract = {
|
||||||
|
id: number;
|
||||||
|
form_id: number;
|
||||||
|
products: ContractProduct;
|
||||||
|
form: Form;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
payment_method: string;
|
||||||
|
cheque_quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ContractCreate = {
|
export type ContractCreate = {
|
||||||
form_id: number;
|
form_id: number;
|
||||||
contract: Record<string, string | number | null>;
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
payment_method: string;
|
||||||
|
cheque_quantity: number;
|
||||||
|
products: ContractProductCreate[];
|
||||||
|
cheques: Cheque[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ContractInputs = {
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
products: Record<string, string | number>;
|
||||||
|
payment_method: string;
|
||||||
|
cheques: Cheque[];
|
||||||
|
cheque_quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContractProduct = {
|
||||||
|
id: number;
|
||||||
|
product_id: number;
|
||||||
|
shipment_id: number;
|
||||||
|
quantity: number;
|
||||||
|
contract: Contract;
|
||||||
|
product: Product;
|
||||||
|
shipment?: Shipment | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContractProductCreate = {
|
||||||
|
product_id: number;
|
||||||
|
shipment_id: number | null;
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function tranformProducts(
|
||||||
|
products: Record<string, string | number>,
|
||||||
|
): ContractProductCreate[] {
|
||||||
|
return Object.entries(products).map(([key, value]) => {
|
||||||
|
const quantity = value;
|
||||||
|
const parts = key.split("-");
|
||||||
|
const shipment_id = parts[0] === "planned" ? Number(parts[1]) : null;
|
||||||
|
const product_id = parts[0] === "planned" ? Number(parts[2]) : Number(parts[1]);
|
||||||
|
return {
|
||||||
|
quantity: Number(quantity),
|
||||||
|
shipment_id: shipment_id,
|
||||||
|
product_id: product_id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user