add contract pdf generation
This commit is contained in:
@@ -13,7 +13,9 @@
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```console
|
```console
|
||||||
pip install backend
|
apt install weasyprint
|
||||||
|
hatch shell
|
||||||
|
fastapi dev src/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ dependencies = [
|
|||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"PyJWT",
|
"PyJWT",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
"requests"
|
"requests",
|
||||||
|
"weasyprint",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import jwt
|
|||||||
from jwt import PyJWKClient
|
from jwt import PyJWKClient
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from src.messages import tokenExpired, invalidToken
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth")
|
router = APIRouter(prefix="/auth")
|
||||||
|
|
||||||
jwk_client = PyJWKClient(JWKS_URL)
|
jwk_client = PyJWKClient(JWKS_URL)
|
||||||
@@ -78,9 +80,9 @@ def verify_token(token: str):
|
|||||||
)
|
)
|
||||||
return payload
|
return payload
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
raise HTTPException(status_code=401, detail="Token expired")
|
raise HTTPException(status_code=401, detail=tokenExpired)
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
raise HTTPException(status_code=401, detail="Invalid token")
|
raise HTTPException(status_code=401, detail=invalidToken)
|
||||||
|
|
||||||
|
|
||||||
def get_current_user(
|
def get_current_user(
|
||||||
|
|||||||
@@ -1,3 +1,108 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from src.database import get_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
|
||||||
|
import src.models as models
|
||||||
|
from src.messages import PDFerrorOccured
|
||||||
|
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):
|
||||||
|
result = 0
|
||||||
|
for product_quantity in products_quantities:
|
||||||
|
product = product_quantity['product']
|
||||||
|
quantity = product_quantity['quantity']
|
||||||
|
result += compute_product_price(product, quantity, nb_shipment)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def compute_planned_prices(planned: list[dict]):
|
||||||
|
result = 0
|
||||||
|
for plan in planned:
|
||||||
|
result += plan['price']
|
||||||
|
return result
|
||||||
|
|
||||||
|
@router.post('/')
|
||||||
|
async def create_contract(
|
||||||
|
contract: models.ContractBase,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
form = form_service.get_one(session, contract.form_id)
|
||||||
|
planned, recurrent = extract_products(session, contract.contract)
|
||||||
|
recurrent_price = compute_recurrent_prices(recurrent, len(form.shipments))
|
||||||
|
total_price = '{:10.2f}'.format(recurrent_price + compute_planned_prices(planned))
|
||||||
|
# TODO: Store contract
|
||||||
|
# TODO: send contract to referer
|
||||||
|
# TODO: Store contract informations ?
|
||||||
|
try:
|
||||||
|
pdf_bytes = generate_html_contract(
|
||||||
|
form,
|
||||||
|
contract.contract,
|
||||||
|
planned,
|
||||||
|
recurrent,
|
||||||
|
'{:10.2f}'.format(recurrent_price),
|
||||||
|
total_price
|
||||||
|
)
|
||||||
|
pdf_file = io.BytesIO(pdf_bytes)
|
||||||
|
contract_id = f'{contract.contract['firstname']}_{contract.contract['lastname']}_{form.productor.type}_{form.season}'
|
||||||
|
except:
|
||||||
|
raise HTTPException(status_code=400, detail=PDFerrorOccured)
|
||||||
|
return StreamingResponse(
|
||||||
|
pdf_file,
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachement; filename=contract_{contract_id}.pdf"
|
||||||
|
}
|
||||||
|
)
|
||||||
58
backend/src/contracts/generate_contract.py
Normal file
58
backend/src/contracts/generate_contract.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
import jinja2
|
||||||
|
import src.models as models
|
||||||
|
import html
|
||||||
|
from weasyprint import HTML
|
||||||
|
|
||||||
|
def generate_html_contract(
|
||||||
|
form: models.Form,
|
||||||
|
contract_informations: dict,
|
||||||
|
planned: list[dict],
|
||||||
|
recurrent: list[dict],
|
||||||
|
recurrent_price: float,
|
||||||
|
total_price: float,
|
||||||
|
):
|
||||||
|
template_dir = "./src/contracts/templates"
|
||||||
|
template_loader = jinja2.FileSystemLoader(searchpath=template_dir)
|
||||||
|
template_env = jinja2.Environment(loader=template_loader, autoescape=jinja2.select_autoescape(["html", "xml"]))
|
||||||
|
template_file = "layout.html"
|
||||||
|
template = template_env.get_template(template_file)
|
||||||
|
output_text = template.render(
|
||||||
|
contract_name=form.name,
|
||||||
|
contract_type=form.productor.type,
|
||||||
|
contract_season=form.season,
|
||||||
|
referer_name=form.referer.name,
|
||||||
|
referer_email=form.referer.email,
|
||||||
|
productor_name=form.productor.name,
|
||||||
|
productor_address=form.productor.address,
|
||||||
|
payment_methods_map={"cheque": "Ordre du chèque", "transfer": "IBAN (paiement par virements)"},
|
||||||
|
productor_payment_methods=form.productor.payment_methods,
|
||||||
|
member_name=f'{html.escape(contract_informations["firstname"])} {html.escape(contract_informations["lastname"])}',
|
||||||
|
member_email=html.escape(contract_informations["email"]),
|
||||||
|
member_phone=html.escape(contract_informations["phone"]),
|
||||||
|
contract_start_date=form.start,
|
||||||
|
contract_end_date=form.end,
|
||||||
|
planned=planned,
|
||||||
|
recurrent=recurrent,
|
||||||
|
recurrent_price=recurrent_price,
|
||||||
|
total_price=total_price,
|
||||||
|
)
|
||||||
|
options = {
|
||||||
|
'page-size': 'Letter',
|
||||||
|
'margin-top': '0.5in',
|
||||||
|
'margin-right': '0.5in',
|
||||||
|
'margin-bottom': '0.5in',
|
||||||
|
'margin-left': '0.5in',
|
||||||
|
'encoding': "UTF-8",
|
||||||
|
'print-media-type': True,
|
||||||
|
"disable-javascript": True,
|
||||||
|
"disable-external-links": True,
|
||||||
|
'enable-local-file-access': False,
|
||||||
|
"disable-local-file-access": True,
|
||||||
|
"no-images": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return HTML(
|
||||||
|
string=output_text,
|
||||||
|
base_url=template_dir
|
||||||
|
).write_pdf()
|
||||||
309
backend/src/contracts/templates/layout.html
Normal file
309
backend/src/contracts/templates/layout.html
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{contract_name}}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style type="text/css">
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1cm;
|
||||||
|
@bottom-center {
|
||||||
|
content: "Page " counter(page);
|
||||||
|
}
|
||||||
|
@top-center {
|
||||||
|
content: "AMAP Croix Luizet - Contrat";
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 2px solid rgb(140 140 140);
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 8px 0;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #999;
|
||||||
|
padding: 6px 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.signature {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.total-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid #999;
|
||||||
|
padding: 10px;
|
||||||
|
width: 200px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>AMAP Croix Luizet</h1>
|
||||||
|
<h2>67 rue Octavie Villeurbanne - <a href="https://amapcroixluizet.eu">https://amapcroixluizet.eu</a></h2>
|
||||||
|
<h3>Contrat d'engagement solidaire</h3>
|
||||||
|
<h4>Informations contractuelles</h4>
|
||||||
|
<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>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Type de contrat</th>
|
||||||
|
<td>{{contract_type}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Saison du contrat</th>
|
||||||
|
<td>{{contract_season}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Type de contrat</th>
|
||||||
|
<td>{{contract_type}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Référent·e</th>
|
||||||
|
<td>{{referer_name}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email référent·e</th>
|
||||||
|
<td>{{referer_email}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Le/La producteur·trice</th>
|
||||||
|
<td>{{productor_name}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Adresse du producteur·trice</th>
|
||||||
|
<td>{{productor_address}}</td>
|
||||||
|
</tr>
|
||||||
|
{% for method in productor_payment_methods %}
|
||||||
|
<tr>
|
||||||
|
<th>{{payment_methods_map[method.name]}}</th>
|
||||||
|
<td>{{method.details}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<th>L’adhérent·e</th>
|
||||||
|
<td>{{member_name}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email de l’adhérent·e</th>
|
||||||
|
<td>{{member_email}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Téléphone de l'adhérent·e</th>
|
||||||
|
<td>{{member_phone}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>Engagement réciproque</h5>
|
||||||
|
<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 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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>
|
||||||
|
<b>Modalités de livraison</b>
|
||||||
|
</h5>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>
|
||||||
|
En cas d’impossibilité
|
||||||
|
</h5>
|
||||||
|
<ul>
|
||||||
|
<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.
|
||||||
|
</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é.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>Rupture du contrat</h5>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if recurrent|length > 0 %}
|
||||||
|
<div class="container">
|
||||||
|
<h4>Produits récurents (pour chaques livraisons)</h4>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom du produit</th>
|
||||||
|
<th>Prix (€)</th>
|
||||||
|
<th>Prix (€/kg)</th>
|
||||||
|
<th>Poids</th>
|
||||||
|
<th>Quantité</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rec in recurrent %}
|
||||||
|
<tr>
|
||||||
|
<td>{{rec.product.name}}</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.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>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="4">Total</th>
|
||||||
|
<td>{{recurrent_price}}€</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if planned|length > 0 %}
|
||||||
|
<div class="container">
|
||||||
|
<h4>Produits planifiés (par livraison)</h4>
|
||||||
|
{% for plan in planned %}
|
||||||
|
<h5>{{plan.shipment.name}} {{plan.shipment.date}}</h5>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom du produit</th>
|
||||||
|
<th>Prix (€)</th>
|
||||||
|
<th>Prix (€/kg)</th>
|
||||||
|
<th>Poids</th>
|
||||||
|
<th>Quantité</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in plan.products %}
|
||||||
|
<tr>
|
||||||
|
<td>{{product.product.name}}</td>
|
||||||
|
<td>{{product.product.price if product.product.price else ""}}</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>
|
||||||
|
{% endfor%}
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="4">Total</th>
|
||||||
|
<td>{{plan.price}}€</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="total-box">
|
||||||
|
<div class="total-label">Prix Total :</div>
|
||||||
|
<div class="total-price">{{total_price}}€</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Signature producteur-trice</th>
|
||||||
|
<th>Signature adhérent-e</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="signature"></td>
|
||||||
|
<td class="signature"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1 +1,4 @@
|
|||||||
notfound = "Resource was not found."
|
notfound = "Resource was not found."
|
||||||
|
PDFerrorOccured = "An error occured during PDF generation please contact administrator"
|
||||||
|
tokenExpired = "Token expired"
|
||||||
|
invalidToken = "Invalid token"
|
||||||
@@ -20,15 +20,30 @@ class UserUpdate(SQLModel):
|
|||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class PaymentMethodBase(SQLModel):
|
||||||
|
name: str
|
||||||
|
details: str
|
||||||
|
|
||||||
|
class PaymentMethod(PaymentMethodBase, table=True):
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
productor_id: int = Field(foreign_key="productor.id", ondelete="CASCADE")
|
||||||
|
productor: Optional["Productor"] = Relationship(
|
||||||
|
back_populates="payment_methods",
|
||||||
|
)
|
||||||
|
|
||||||
|
class PaymentMethodPublic(PaymentMethodBase):
|
||||||
|
id: int
|
||||||
|
productor: Optional["Productor"]
|
||||||
|
|
||||||
class ProductorBase(SQLModel):
|
class ProductorBase(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
address: str
|
address: str
|
||||||
payment: str
|
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
class ProductorPublic(ProductorBase):
|
class ProductorPublic(ProductorBase):
|
||||||
id: int
|
id: int
|
||||||
products: list["Product"] = []
|
products: list["Product"] = []
|
||||||
|
payment_methods: list["PaymentMethod"] = []
|
||||||
|
|
||||||
class Productor(ProductorBase, table=True):
|
class Productor(ProductorBase, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
@@ -39,15 +54,19 @@ class Productor(ProductorBase, table=True):
|
|||||||
"order_by": "Product.name"
|
"order_by": "Product.name"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
payment_methods: list["PaymentMethod"] = Relationship(
|
||||||
|
back_populates="productor",
|
||||||
|
cascade_delete=True
|
||||||
|
)
|
||||||
|
|
||||||
class ProductorUpdate(SQLModel):
|
class ProductorUpdate(SQLModel):
|
||||||
name: str | None
|
name: str | None
|
||||||
address: str | None
|
address: str | None
|
||||||
payment: str | None
|
payment_methods: list["PaymentMethod"] = []
|
||||||
type: str | None
|
type: str | None
|
||||||
|
|
||||||
class ProductorCreate(ProductorBase):
|
class ProductorCreate(ProductorBase):
|
||||||
pass
|
payment_methods: list["PaymentMethod"] = []
|
||||||
|
|
||||||
class Unit(StrEnum):
|
class Unit(StrEnum):
|
||||||
GRAMS = "1"
|
GRAMS = "1"
|
||||||
@@ -102,6 +121,7 @@ class FormBase(SQLModel):
|
|||||||
season: str
|
season: str
|
||||||
start: datetime.date
|
start: datetime.date
|
||||||
end: datetime.date
|
end: datetime.date
|
||||||
|
minimum_shipment_value: float | None
|
||||||
|
|
||||||
class FormPublic(FormBase):
|
class FormPublic(FormBase):
|
||||||
id: int
|
id: int
|
||||||
@@ -128,6 +148,7 @@ class FormUpdate(SQLModel):
|
|||||||
season: str | None
|
season: str | None
|
||||||
start: datetime.date | None
|
start: datetime.date | None
|
||||||
end: datetime.date | None
|
end: datetime.date | None
|
||||||
|
minimum_shipment_value: float | None
|
||||||
|
|
||||||
class FormCreate(FormBase):
|
class FormCreate(FormBase):
|
||||||
pass
|
pass
|
||||||
@@ -148,13 +169,14 @@ class TemplateCreate(TemplateBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class ContractBase(SQLModel):
|
class ContractBase(SQLModel):
|
||||||
pass
|
form_id: int
|
||||||
|
contract: dict
|
||||||
|
|
||||||
class ContractPublic(ContractBase):
|
class ContractPublic(ContractBase):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Contract(ContractBase, table=True):
|
# class Contract(ContractBase, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
# id: int | None = Field(default=None, primary_key=True)
|
||||||
|
|
||||||
class ContractUpdate(SQLModel):
|
class ContractUpdate(SQLModel):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -13,8 +13,16 @@ def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
|
|||||||
return session.get(models.Productor, productor_id)
|
return session.get(models.Productor, productor_id)
|
||||||
|
|
||||||
def create_one(session: Session, productor: models.ProductorCreate) -> models.ProductorPublic:
|
def create_one(session: Session, productor: models.ProductorCreate) -> models.ProductorPublic:
|
||||||
productor_create = productor.model_dump(exclude_unset=True)
|
productor_create = productor.model_dump(exclude_unset=True, exclude="payment_methods")
|
||||||
new_productor = models.Productor(**productor_create)
|
new_productor = models.Productor(**productor_create)
|
||||||
|
|
||||||
|
new_productor.payment_methods = [
|
||||||
|
models.PaymentMethod(
|
||||||
|
name=pm.name,
|
||||||
|
details=pm.details
|
||||||
|
) for pm in productor.payment_methods
|
||||||
|
]
|
||||||
|
|
||||||
session.add(new_productor)
|
session.add(new_productor)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(new_productor)
|
session.refresh(new_productor)
|
||||||
@@ -26,7 +34,20 @@ def update_one(session: Session, id: int, productor: models.ProductorUpdate) ->
|
|||||||
new_productor = result.first()
|
new_productor = result.first()
|
||||||
if not new_productor:
|
if not new_productor:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
productor_updates = productor.model_dump(exclude_unset=True)
|
productor_updates = productor.model_dump(exclude_unset=True)
|
||||||
|
if "payment_methods" in productor_updates:
|
||||||
|
new_productor.payment_methods.clear()
|
||||||
|
for pm in productor_updates["payment_methods"]:
|
||||||
|
new_productor.payment_methods.append(
|
||||||
|
models.PaymentMethod(
|
||||||
|
name=pm["name"],
|
||||||
|
details=pm["details"],
|
||||||
|
productor_id=id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
del productor_updates["payment_methods"]
|
||||||
|
|
||||||
for key, value in productor_updates.items():
|
for key, value in productor_updates.items():
|
||||||
setattr(new_productor, key, value)
|
setattr(new_productor, key, value)
|
||||||
session.add(new_productor)
|
session.add(new_productor)
|
||||||
|
|||||||
BIN
backend/test.pdf
Normal file
BIN
backend/test.pdf
Normal file
Binary file not shown.
@@ -39,7 +39,9 @@
|
|||||||
"filter by name": "filter by name",
|
"filter by name": "filter by name",
|
||||||
"filter by type": "filter by type",
|
"filter by type": "filter by type",
|
||||||
"address": "address",
|
"address": "address",
|
||||||
"payment": "payment",
|
"payment methods": "payment methods",
|
||||||
|
"cheque": "cheque",
|
||||||
|
"transfer": "transfer",
|
||||||
"type": "type",
|
"type": "type",
|
||||||
"create productor": "create productor",
|
"create productor": "create productor",
|
||||||
"productor name": "productor name",
|
"productor name": "productor name",
|
||||||
@@ -61,6 +63,13 @@
|
|||||||
"there is": "there is",
|
"there is": "there is",
|
||||||
"for this contract": "for this contact.",
|
"for this contract": "for this contact.",
|
||||||
"shipment date": "shipment date",
|
"shipment date": "shipment date",
|
||||||
|
"shipment products": "shipment products",
|
||||||
|
"minimum shipment value": "minimum shipment value",
|
||||||
|
"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)",
|
||||||
|
"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).",
|
||||||
|
"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.",
|
||||||
|
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
|
||||||
"remove shipment": "remove shipment",
|
"remove shipment": "remove shipment",
|
||||||
"productors": "productors",
|
"productors": "productors",
|
||||||
"products": "products",
|
"products": "products",
|
||||||
@@ -83,5 +92,34 @@
|
|||||||
"a lastname": "a lastname",
|
"a lastname": "a lastname",
|
||||||
"a email": "a email",
|
"a email": "a email",
|
||||||
"submit contract": "submit contract",
|
"submit contract": "submit contract",
|
||||||
|
"success": "success",
|
||||||
|
"successfully edited user": "successfully edited user",
|
||||||
|
"successfully edited form": "successfully edited form",
|
||||||
|
"successfully edited product": "successfully edited product",
|
||||||
|
"successfully edited productor": "successfully edited productor",
|
||||||
|
"successfully edited shipment": "successfully edited shipment",
|
||||||
|
"successfully created user": "successfully created user",
|
||||||
|
"successfully created form": "successfully created form",
|
||||||
|
"successfully created product": "successfully created product",
|
||||||
|
"successfully created productor": "successfully created productor",
|
||||||
|
"successfully created shipment": "successfully created shipment",
|
||||||
|
"error": "error",
|
||||||
|
"error editing user": "error editing user",
|
||||||
|
"error editing form": "error editing form",
|
||||||
|
"error editing product": "error editing product",
|
||||||
|
"error editing productor": "error editing productor",
|
||||||
|
"error editing shipment": "error editing shipment",
|
||||||
|
"error creating user": "error creating user",
|
||||||
|
"error creating form": "error creating form",
|
||||||
|
"error creating product": "error creating product",
|
||||||
|
"error creating productor": "error creating productor",
|
||||||
|
"error creating shipment": "error creating shipment",
|
||||||
|
"error deleting user": "error deleting user",
|
||||||
|
"error deleting form": "error deleting form",
|
||||||
|
"error deleting product": "error deleting product",
|
||||||
|
"error deleting productor": "error deleting productor",
|
||||||
|
"error deleting shipment": "error deleting shipment",
|
||||||
|
"there is no contract for now": "there is no contract for now.",
|
||||||
|
"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",
|
||||||
"all theses informations are for contract generation, no informations is stored outside of contracts": "all theses informations are for contract generation, no informations is stored outside of contracts."
|
"all theses informations are for contract generation, no informations is stored outside of contracts": "all theses informations are for contract generation, no informations is stored outside of contracts."
|
||||||
}
|
}
|
||||||
@@ -2,17 +2,16 @@
|
|||||||
"product name": "nom du produit",
|
"product name": "nom du produit",
|
||||||
"product price": "prix du produit",
|
"product price": "prix du produit",
|
||||||
"product quantity": "quantité du produit",
|
"product quantity": "quantité du produit",
|
||||||
|
"product quantity unit": "Unité de quantité du produit",
|
||||||
"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": "Selectionnez les produits pour chaque livraison.",
|
||||||
"recurrent": "récurrent",
|
"recurrent": "récurent",
|
||||||
"recurrent products": "Produits récurents",
|
"recurrent products": "Produits récurents",
|
||||||
"your selection in this category will apply for all shipments": "votre selection sera appliquée pour chaque livraisons (Exemple: 6 livraisons, le produits sera comptés 6 fois : une fois par livraison).",
|
"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).",
|
||||||
"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",
|
||||||
"grams": "grammes",
|
|
||||||
"kilo": "kilo",
|
|
||||||
"piece": "pièce",
|
"piece": "pièce",
|
||||||
"in": "en",
|
"in": "en",
|
||||||
"enter quantity": "entrez la quantitée",
|
"enter quantity": "entrez la quantitée",
|
||||||
@@ -33,20 +32,24 @@
|
|||||||
"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 formulare de contrat",
|
||||||
"edit productor": "modifier le producteur·trice",
|
"create productor": "créer le/la producteur·trice",
|
||||||
"remove productor": "supprimer le producteur·trice",
|
"edit productor": "modifier le/la producteur·trice",
|
||||||
|
"remove productor": "supprimer le/la producteur·trice",
|
||||||
"home": "accueil",
|
"home": "accueil",
|
||||||
"dashboard": "tableau de bord",
|
"dashboard": "tableau de bord",
|
||||||
"filter by name": "filtrer par nom",
|
"filter by name": "filtrer par nom",
|
||||||
"filter by type": "filtrer par type",
|
"filter by type": "filtrer par type",
|
||||||
"address": "adresse",
|
"address": "adresse",
|
||||||
"payment": "ordre du chèque",
|
"payment methods": "méthodes de paiement",
|
||||||
"type": "type",
|
"type": "type",
|
||||||
"create productor": "créer le producteur·trice",
|
"cheque": "chèque",
|
||||||
|
"transfer": "virement",
|
||||||
|
"order name": "Ordre du chèque",
|
||||||
|
"IBAN": "IBAN",
|
||||||
"productor name": "nom du producteur·trice",
|
"productor name": "nom du producteur·trice",
|
||||||
"productor type": "type du producteur·trice",
|
"productor type": "type du producteur·trice",
|
||||||
"productor address": "adresse du producteur·trice",
|
"productor address": "adresse du producteur·trice",
|
||||||
"productor payment": "ordre du chèque du producteur·trice",
|
"productor payment": "méthodes de paiement du producteur·trice",
|
||||||
"priceKg": "prix au kilo",
|
"priceKg": "prix au kilo",
|
||||||
"quantity": "quantité",
|
"quantity": "quantité",
|
||||||
"quantity unit": "unité de quantité",
|
"quantity unit": "unité de quantité",
|
||||||
@@ -60,6 +63,13 @@
|
|||||||
"shipment date": "date de la livraison",
|
"shipment date": "date de la livraison",
|
||||||
"shipments": "livraisons",
|
"shipments": "livraisons",
|
||||||
"shipment": "livraison",
|
"shipment": "livraison",
|
||||||
|
"shipment products": "produits pour la livraison",
|
||||||
|
"shipment form": "formulaire lié a la 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).",
|
||||||
|
"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).",
|
||||||
|
"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",
|
||||||
"there is": "il y a",
|
"there is": "il y a",
|
||||||
"for this contract": "pour ce contrat.",
|
"for this contract": "pour ce contrat.",
|
||||||
"remove shipment": "supprimer la livraison",
|
"remove shipment": "supprimer la livraison",
|
||||||
@@ -79,11 +89,50 @@
|
|||||||
"a start date": "une date de début",
|
"a start date": "une date de début",
|
||||||
"a end date": "une date de fin",
|
"a end date": "une date de fin",
|
||||||
"a productor": "un(e) producteur·trice",
|
"a productor": "un(e) producteur·trice",
|
||||||
"a referer": "un référent·e",
|
"a referer": "un(e) référent·e",
|
||||||
"a phone": "un numéro de téléphone",
|
"a phone": "un numéro de téléphone",
|
||||||
"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",
|
||||||
"submit contract": "Envoyer le contrat",
|
"submit contract": "Envoyer le contrat",
|
||||||
|
"mililiter": "mililitres (ml)",
|
||||||
|
"grams": "grammes (g)",
|
||||||
|
"kilo": "kilogrammes (kg)",
|
||||||
|
"liter": "litres (L)",
|
||||||
|
"success": "succès",
|
||||||
|
"successfully edited user": "utilisateur·trice correctement édité",
|
||||||
|
"successfully edited form": "formulaire correctement édité",
|
||||||
|
"successfully edited product": "produit correctement édité",
|
||||||
|
"successfully edited productor": "producteur·trice correctement édité(e)",
|
||||||
|
"successfully edited shipment": "livaison correctement éditée",
|
||||||
|
"successfully created user": "utilisateur·trice correctement créé(e)",
|
||||||
|
"successfully created form": "formulaire correctement créé",
|
||||||
|
"successfully created product": "produit correctement créé",
|
||||||
|
"successfully created productor": "producteur·trice correctement créé(e)",
|
||||||
|
"successfully created shipment": "livaison correctement créée",
|
||||||
|
"successfully deleted user": "utilisateur·trice correctement supprimé",
|
||||||
|
"successfully deleted form": "formulaire correctement supprimé",
|
||||||
|
"successfully deleted product": "produit correctement supprimé",
|
||||||
|
"successfully deleted productor": "producteur·trice correctement supprimé(e)",
|
||||||
|
"successfully deleted shipment": "livaison correctement supprimée",
|
||||||
|
"error": "erreur",
|
||||||
|
"error editing user": "erreur pendant l'édition de l'utilisateur·trice",
|
||||||
|
"error editing form": "erreur pendant l'édition du formulaire",
|
||||||
|
"error editing product": "erreur pendant l'édition du produit",
|
||||||
|
"error editing productor": "erreur pendant l'édition du producteur·trice",
|
||||||
|
"error editing shipment": "erreur pendant l'édition de la livraison",
|
||||||
|
"error creating user": "erreur pendant la création de l'utilisateur·trice",
|
||||||
|
"error creating form": "erreur pendant la création du formulaire",
|
||||||
|
"error creating product": "erreur pendant la création du produit",
|
||||||
|
"error creating productor": "erreur pendant la création du producteur·trice",
|
||||||
|
"error creating shipment": "erreur pendant la création de la livraison",
|
||||||
|
"error deleting user": "erreur pendant la suppression de l'utilisateur·trice",
|
||||||
|
"error deleting form": "erreur pendant la suppression du formulaire",
|
||||||
|
"error deleting product": "erreur pendant la suppression du produit",
|
||||||
|
"error deleting productor": "erreur pendant la suppression du producteur·trice",
|
||||||
|
"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.",
|
||||||
|
|
||||||
|
"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.",
|
||||||
"all theses informations are for contract generation, no informations is stored outside of contracts": "ces informations sont nécéssaires pour la génération de contrat, aucune information personnelle n'est gardée ailleurs que dans les contrats générés."
|
"all theses informations are for contract generation, no informations is stored outside of contracts": "ces informations sont nécéssaires pour la génération de contrat, aucune information personnelle n'est gardée ailleurs que dans les contrats générés."
|
||||||
}
|
}
|
||||||
92
frontend/package-lock.json
generated
92
frontend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@mantine/dates": "^8.3.14",
|
"@mantine/dates": "^8.3.14",
|
||||||
"@mantine/form": "^8.3.14",
|
"@mantine/form": "^8.3.14",
|
||||||
"@mantine/hooks": "^8.3.14",
|
"@mantine/hooks": "^8.3.14",
|
||||||
|
"@mantine/notifications": "^8.3.14",
|
||||||
"@tabler/icons": "^3.36.1",
|
"@tabler/icons": "^3.36.1",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
@@ -1147,6 +1148,31 @@
|
|||||||
"react": "^18.x || ^19.x"
|
"react": "^18.x || ^19.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mantine/notifications": {
|
||||||
|
"version": "8.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.14.tgz",
|
||||||
|
"integrity": "sha512-+ia97wrcU9Zfv+jXYvgr2GdISqKTHbQE9nnEIZvGUBPAqKr9b2JAsaXQS/RsAdoXUI+kKDEtH2fyVYS7zrSi/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/store": "8.3.14",
|
||||||
|
"react-transition-group": "4.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mantine/core": "8.3.14",
|
||||||
|
"@mantine/hooks": "8.3.14",
|
||||||
|
"react": "^18.x || ^19.x",
|
||||||
|
"react-dom": "^18.x || ^19.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mantine/store": {
|
||||||
|
"version": "8.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.14.tgz",
|
||||||
|
"integrity": "sha512-bgW+fYHDOp7Pk4+lcEm3ZF7dD/sIMKHyR985cOqSHAYJPRcVFb+zcEK/SWoFZqlyA4qh08CNrASOaod8N0XKfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.x || ^19.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.3",
|
"version": "1.0.0-rc.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
@@ -2226,7 +2252,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
@@ -2266,6 +2291,16 @@
|
|||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.286",
|
"version": "1.5.286",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
||||||
@@ -2818,7 +2853,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -2937,6 +2971,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -3009,6 +3055,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -3277,6 +3332,17 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3335,6 +3401,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-number-format": {
|
"node_modules/react-number-format": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz",
|
||||||
@@ -3463,6 +3535,22 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@mantine/dates": "^8.3.14",
|
"@mantine/dates": "^8.3.14",
|
||||||
"@mantine/form": "^8.3.14",
|
"@mantine/form": "^8.3.14",
|
||||||
"@mantine/hooks": "^8.3.14",
|
"@mantine/hooks": "^8.3.14",
|
||||||
|
"@mantine/notifications": "^8.3.14",
|
||||||
"@tabler/icons": "^3.36.1",
|
"@tabler/icons": "^3.36.1",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function FormCard({form}: FormCardProps) {
|
|||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/form/${form.id}`}
|
to={`/form/${form.id}`}
|
||||||
|
style={{textDecoration: "none", color: "black"}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Title
|
<Title
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Group, Modal, Select, TextInput, type ModalBaseProps } from "@mantine/core";
|
import { Button, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } 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 { getProductors, getUsers } from "@/services/api";
|
import { getProductors, getUsers } from "@/services/api";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
@@ -20,6 +20,7 @@ export default function FormModal({
|
|||||||
}: FormModalProps) {
|
}: FormModalProps) {
|
||||||
const {data: productors} = getProductors();
|
const {data: productors} = getProductors();
|
||||||
const {data: users} = getUsers();
|
const {data: users} = getUsers();
|
||||||
|
|
||||||
const form = useForm<FormInputs>({
|
const form = useForm<FormInputs>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -28,6 +29,7 @@ export default function FormModal({
|
|||||||
end: null,
|
end: null,
|
||||||
productor_id: "",
|
productor_id: "",
|
||||||
referer_id: "",
|
referer_id: "",
|
||||||
|
minimum_shipment_value: null,
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
name: (value) =>
|
name: (value) =>
|
||||||
@@ -67,10 +69,9 @@ export default function FormModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={currentForm ? t("edit form") : t('create form')}
|
title={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("form name", {capfirst: true})}
|
label={t("form name", {capfirst: true})}
|
||||||
@@ -123,6 +124,13 @@ export default function FormModal({
|
|||||||
data={productorsSelect || []}
|
data={productorsSelect || []}
|
||||||
{...form.getInputProps('productor_id')}
|
{...form.getInputProps('productor_id')}
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t("minimum shipment value", {capfirst: true})}
|
||||||
|
placeholder={t("minimum shipment value", {capfirst: true})}
|
||||||
|
description={t("some contracts require a minimum value per shipment, ignore this field if it's not the case", {capfirst: true})}
|
||||||
|
radius="sm"
|
||||||
|
{...form.getInputProps('minimum_shipment_value')}
|
||||||
|
/>
|
||||||
<Group mt="sm" justify="space-between">
|
<Group mt="sm" justify="space-between">
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@@ -137,6 +145,7 @@ export default function FormModal({
|
|||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||||
|
leftSection={currentForm ? <IconEdit/> : <IconPlus/>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
|
|||||||
28
frontend/src/components/Label/index.tsx
Normal file
28
frontend/src/components/Label/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||||
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export type InputLabelProps = {
|
||||||
|
label: string;
|
||||||
|
info: string;
|
||||||
|
isRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputLabel({label, info, isRequired}: InputLabelProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<Tooltip label={info}>
|
||||||
|
<ActionIcon variant="transparent" size="xs" color="gray">
|
||||||
|
<IconInfoCircle size={16}/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
{label}
|
||||||
|
{
|
||||||
|
isRequired ?
|
||||||
|
<span style={{ color: 'red' }}> *</span> : null
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
justify-self: left;
|
justify-self: left;
|
||||||
width: 50%;
|
padding: 1rem;
|
||||||
|
background-color: var(--mantine-color-blue-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.navLink {
|
||||||
gap: 1em;
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,18 @@ import "./index.css";
|
|||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
return (
|
return (
|
||||||
<nav>
|
<nav>
|
||||||
<NavLink to="/">{t("home", {capfirst: true})}</NavLink>
|
<NavLink
|
||||||
<NavLink to="/dashboard/productors">{t("dashboard", {capfirst: true})}</NavLink>
|
className={"navLink"}
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
{t("home", {capfirst: true})}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
className={"navLink"}
|
||||||
|
to="/dashboard/productors"
|
||||||
|
>
|
||||||
|
{t("dashboard", {capfirst: true})}
|
||||||
|
</NavLink>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
import { Button, Group, Modal, MultiSelect, 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 } from "@tabler/icons-react";
|
||||||
import type { Productor, ProductorInputs } from "@/services/resources/productors";
|
import { PaymentMethods, type Productor, type ProductorInputs } from "@/services/resources/productors";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export type ProductorModalProps = ModalBaseProps & {
|
export type ProductorModalProps = ModalBaseProps & {
|
||||||
@@ -20,18 +20,16 @@ export function ProductorModal({
|
|||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
address: "",
|
address: "",
|
||||||
payment: "",
|
payment_methods: [],
|
||||||
type: "",
|
type: "",
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
name: (value) =>
|
name: (value) =>
|
||||||
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
|
!value ? `${t("name", {capfirst: true})} ${t("is required")}` : null,
|
||||||
address: (value) =>
|
address: (value) =>
|
||||||
!value ? `${t("address", {capfirst: true})} ${t('is required')}` : null,
|
!value ? `${t("address", {capfirst: true})} ${t("is required")}` : null,
|
||||||
payment: (value) =>
|
|
||||||
!value ? `${t("payment", {capfirst: true})} ${t('is required')}` : null,
|
|
||||||
type: (value) =>
|
type: (value) =>
|
||||||
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null
|
!value ? `${t("type", {capfirst: true})} ${t("is required")}` : null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ export function ProductorModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("create productor", {capfirst: true})}
|
title={t("create productor", {capfirst: true})}
|
||||||
@@ -56,30 +53,65 @@ export function ProductorModal({
|
|||||||
placeholder={t("productor name", {capfirst: true})}
|
placeholder={t("productor name", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("productor type", {capfirst: true})}
|
label={t("productor type", {capfirst: true})}
|
||||||
placeholder={t("productor type", {capfirst: true})}
|
placeholder={t("productor type", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
{...form.getInputProps('type')}
|
{...form.getInputProps("type")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("productor address", {capfirst: true})}
|
label={t("productor address", {capfirst: true})}
|
||||||
placeholder={t("productor address", {capfirst: true})}
|
placeholder={t("productor address", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
{...form.getInputProps('address')}
|
{...form.getInputProps("address")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<MultiSelect
|
||||||
label={t("productor payment", {capfirst: true})}
|
label={t("payment methods", {capfirst: true})}
|
||||||
placeholder={t("productor payment", {capfirst: true})}
|
placeholder={t("payment methods", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
{...form.getInputProps('payment')}
|
data={PaymentMethods}
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
value={form.values.payment_methods.map(p => p.name)}
|
||||||
|
onChange={(names) => {
|
||||||
|
form.setFieldValue("payment_methods", names.map(name => {
|
||||||
|
const existing = form.values.payment_methods.find(p => p.name === name);
|
||||||
|
return existing ?? {
|
||||||
|
name,
|
||||||
|
details: ""
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
form.values.payment_methods.map((method, index) => (
|
||||||
|
<TextInput
|
||||||
|
key={index}
|
||||||
|
label={
|
||||||
|
method.name === "cheque" ?
|
||||||
|
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`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
<Group mt="sm" justify="space-between">
|
<Group mt="sm" justify="space-between">
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@@ -93,14 +125,15 @@ export function ProductorModal({
|
|||||||
>{t("cancel", {capfirst: true})}</Button>
|
>{t("cancel", {capfirst: true})}</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="filled"
|
variant="filled"
|
||||||
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}
|
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
|
console.log(form.getValues())
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
handleSubmit(form.getValues(), currentProductor?.id)
|
handleSubmit(form.getValues(), currentProductor?.id)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button>
|
>{currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ActionIcon, Table, Tooltip } from "@mantine/core";
|
import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { IconEdit, IconX } from "@tabler/icons-react";
|
import { IconEdit, IconX } from "@tabler/icons-react";
|
||||||
import type { Productor } from "@/services/resources/productors";
|
import type { Productor } from "@/services/resources/productors";
|
||||||
@@ -21,7 +21,15 @@ export default function ProductorRow({
|
|||||||
<Table.Td>{productor.name}</Table.Td>
|
<Table.Td>{productor.name}</Table.Td>
|
||||||
<Table.Td>{productor.type}</Table.Td>
|
<Table.Td>{productor.type}</Table.Td>
|
||||||
<Table.Td>{productor.address}</Table.Td>
|
<Table.Td>{productor.address}</Table.Td>
|
||||||
<Table.Td>{productor.payment}</Table.Td>
|
<Table.Td>
|
||||||
|
{
|
||||||
|
productor.payment_methods.map((value) =>(
|
||||||
|
<Badge ml="xs">
|
||||||
|
{t(value.name, {capfirst: true})}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Tooltip label={t("edit productor", {capfirst: true})}>
|
<Tooltip label={t("edit productor", {capfirst: true})}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Button, Group, Modal, NumberInput, Pill, Select, TextInput, Title, Tooltip, type ModalBaseProps } from "@mantine/core";
|
import { Button, Group, Modal, NumberInput, Select, 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, IconInfoCircle } from "@tabler/icons-react";
|
import { IconCancel } from "@tabler/icons-react";
|
||||||
import { ProductQuantityUnit, productToProductInputs, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products";
|
import { ProductQuantityUnit, productToProductInputs, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { getProductors } from "@/services/api";
|
import { getProductors } from "@/services/api";
|
||||||
|
import { InputLabel } from "@/components/Label";
|
||||||
|
|
||||||
export type ProductModalProps = ModalBaseProps & {
|
export type ProductModalProps = ModalBaseProps & {
|
||||||
currentProduct?: Product;
|
currentProduct?: Product;
|
||||||
@@ -42,7 +43,7 @@ export function ProductModal({
|
|||||||
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
|
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
|
||||||
productor_id: (value) =>
|
productor_id: (value) =>
|
||||||
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
|
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,7 +58,6 @@ export function ProductModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("create product", {capfirst: true})}
|
title={t("create product", {capfirst: true})}
|
||||||
@@ -78,19 +78,23 @@ export function ProductModal({
|
|||||||
label={t("product name", {capfirst: true})}
|
label={t("product name", {capfirst: true})}
|
||||||
placeholder={t("product name", {capfirst: true})}
|
placeholder={t("product name", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label={t("product type", {capfirst: true})}
|
label={
|
||||||
|
<InputLabel
|
||||||
|
label={t("product type", {capfirst: true})}
|
||||||
|
info={t("recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)", {capfirst: true})}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
}
|
||||||
placeholder={t("product type", {capfirst: true})}
|
placeholder={t("product type", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
withAsterisk
|
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
data={[
|
data={[
|
||||||
{value: "1", label: t("planned")},
|
{value: "1", label: t("planned", {capfirst: true})},
|
||||||
{value: "2", label: t("recurrent")}
|
{value: "2", label: t("recurrent", {capfirst: true})}
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('type')}
|
{...form.getInputProps('type')}
|
||||||
/>
|
/>
|
||||||
@@ -103,7 +107,7 @@ export function ProductModal({
|
|||||||
withAsterisk
|
withAsterisk
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value)}))}
|
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
|
||||||
{...form.getInputProps('unit')}
|
{...form.getInputProps('unit')}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
@@ -131,7 +135,9 @@ export function ProductModal({
|
|||||||
label={t("product quantity unit", {capfirst: true})}
|
label={t("product quantity unit", {capfirst: true})}
|
||||||
placeholder={t("product quantity unit", {capfirst: true})}
|
placeholder={t("product quantity unit", {capfirst: true})}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value)}))}
|
clearable
|
||||||
|
searchable
|
||||||
|
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
|
||||||
{...form.getInputProps('quantity_unit', {capfirst: true})}
|
{...form.getInputProps('quantity_unit', {capfirst: true})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -152,7 +158,6 @@ export function ProductModal({
|
|||||||
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
|
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.validate();
|
form.validate();
|
||||||
console.log(form.isValid(), form.getValues())
|
|
||||||
if (form.isValid()) {
|
if (form.isValid()) {
|
||||||
handleSubmit(form.getValues(), currentProduct?.id)
|
handleSubmit(form.getValues(), currentProduct?.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,27 @@ export default function ProductRow({
|
|||||||
<Table.Tr key={product.id}>
|
<Table.Tr key={product.id}>
|
||||||
<Table.Td>{product.name}</Table.Td>
|
<Table.Td>{product.name}</Table.Td>
|
||||||
<Table.Td>{t(ProductType[product.type])}</Table.Td>
|
<Table.Td>{t(ProductType[product.type])}</Table.Td>
|
||||||
<Table.Td>{product.price}</Table.Td>
|
<Table.Td>
|
||||||
<Table.Td>{product.price_kg}</Table.Td>
|
{
|
||||||
<Table.Td>{product.quantity}</Table.Td>
|
product.price ?
|
||||||
<Table.Td>{product.quantity_unit}</Table.Td>
|
Intl.NumberFormat(
|
||||||
<Table.Td>{t(ProductUnit[product.unit])}</Table.Td>
|
"fr-FR",
|
||||||
|
{style: "currency", currency: "EUR"}
|
||||||
|
).format(product.price) : null
|
||||||
|
}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{
|
||||||
|
product.price_kg ?
|
||||||
|
`${Intl.NumberFormat(
|
||||||
|
"fr-FR",
|
||||||
|
{style: "currency", currency: "EUR"}
|
||||||
|
).format(product.price_kg)}/kg` : null
|
||||||
|
|
||||||
|
}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{product.quantity}{product.quantity_unit}</Table.Td>
|
||||||
|
<Table.Td>{t(ProductUnit[product.unit], {capfirst: true})}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Tooltip label={t("edit product", {capfirst: true})}>
|
<Tooltip label={t("edit product", {capfirst: true})}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Accordion, Group, Text } from "@mantine/core";
|
import { Accordion, Group, Stack, Text } from "@mantine/core";
|
||||||
import type { Shipment } from "@/services/resources/shipments";
|
import type { Shipment } from "@/services/resources/shipments";
|
||||||
import { ProductForm } from "@/components/Products/Form";
|
import { ProductForm } from "@/components/Products/Form";
|
||||||
import type { UseFormReturnType } from "@mantine/form";
|
import type { UseFormReturnType } from "@mantine/form";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { computePrices } from "@/pages/Contract";
|
import { computePrices } from "@/pages/Contract";
|
||||||
|
import { t } from "@/config/i18n";
|
||||||
|
|
||||||
export type ShipmentFormProps = {
|
export type ShipmentFormProps = {
|
||||||
inputForm: UseFormReturnType<Record<string, string | number>>;
|
inputForm: UseFormReturnType<Record<string, string | number>>;
|
||||||
shipment: Shipment;
|
shipment: Shipment;
|
||||||
|
minimumPrice?: number | null;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ export default function ShipmentForm({
|
|||||||
shipment,
|
shipment,
|
||||||
index,
|
index,
|
||||||
inputForm,
|
inputForm,
|
||||||
|
minimumPrice,
|
||||||
}: ShipmentFormProps) {
|
}: ShipmentFormProps) {
|
||||||
const shipmentPrice = useMemo(() => {
|
const shipmentPrice = useMemo(() => {
|
||||||
const values = Object
|
const values = Object
|
||||||
@@ -26,18 +29,42 @@ export default function ShipmentForm({
|
|||||||
return computePrices(values, shipment.products);
|
return computePrices(values, shipment.products);
|
||||||
}, [inputForm, shipment.products]);
|
}, [inputForm, shipment.products]);
|
||||||
|
|
||||||
|
const priceRequirement = useMemo(() => {
|
||||||
|
if (!minimumPrice)
|
||||||
|
return false;
|
||||||
|
return minimumPrice ? shipmentPrice < minimumPrice : true
|
||||||
|
}, [shipmentPrice, minimumPrice])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion.Item value={String(index)}>
|
<Accordion.Item value={String(index)}>
|
||||||
<Accordion.Control>
|
<Accordion.Control>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Text>{shipment.name}</Text>
|
<Text>{shipment.name}</Text>
|
||||||
<Text>{
|
<Stack gap={0}>
|
||||||
|
<Text c={priceRequirement ? "red" : "green"}>{
|
||||||
Intl.NumberFormat(
|
Intl.NumberFormat(
|
||||||
"fr-FR",
|
"fr-FR",
|
||||||
{style: "currency", currency: "EUR"}
|
{style: "currency", currency: "EUR"}
|
||||||
).format(shipmentPrice)
|
).format(shipmentPrice)
|
||||||
}</Text>
|
}</Text>
|
||||||
<Text mr="lg">{shipment.date}</Text>
|
{
|
||||||
|
priceRequirement ?
|
||||||
|
<Text c="red"size="sm">
|
||||||
|
{`${t("minimum price for this shipment should be at least", {capfirst: true})} ${minimumPrice}€`}
|
||||||
|
</Text> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
<Text mr="lg">
|
||||||
|
{`${
|
||||||
|
new Date(shipment.date).toLocaleDateString("fr-FR", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
}`}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ export default function ShipmentModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={currentShipment ? t("edit shipment") : t('create shipment')}
|
title={currentShipment ? t("edit shipment") : t('create shipment')}
|
||||||
@@ -93,6 +92,7 @@ export default function ShipmentModal({
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={t("shipment products", {capfirst: true})}
|
label={t("shipment products", {capfirst: true})}
|
||||||
placeholder={t("shipment products", {capfirst: true})}
|
placeholder={t("shipment products", {capfirst: true})}
|
||||||
|
description={t("shipment products is necessary only for planned products (if all products are recurrent leave empty)", {capfirst: true})}
|
||||||
data={productsSelect || []}
|
data={productsSelect || []}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function UserModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("create user", {capfirst: true})}
|
title={t("create user", {capfirst: true})}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { MantineProvider } from "@mantine/core";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
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 { Notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
|
<Notifications />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ProductForm } from "@/components/Products/Form";
|
import { ProductForm } from "@/components/Products/Form";
|
||||||
import ShipmentForm from "@/components/Shipments/Form";
|
import ShipmentForm from "@/components/Shipments/Form";
|
||||||
import { t } from "@/config/i18n";
|
import { t } from "@/config/i18n";
|
||||||
import { getForm } from "@/services/api";
|
import { createContract, getForm } from "@/services/api";
|
||||||
import { type Product } from "@/services/resources/products";
|
import { type Product } from "@/services/resources/products";
|
||||||
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
|
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
|
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
export function computePrices(values: [string, any][], products: Product[], nbShipment?: number) {
|
export function computePrices(values: [string, any][], products: Product[], nbShipment?: number) {
|
||||||
@@ -15,7 +15,7 @@ export function computePrices(values: [string, any][], products: Product[], nbSh
|
|||||||
const productId = Number(keyArray[keyArray.length - 1]);
|
const productId = Number(keyArray[keyArray.length - 1]);
|
||||||
const product = products.find((product) => product.id === productId);
|
const product = products.find((product) => product.id === productId);
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return 0;
|
return prev + 0;
|
||||||
}
|
}
|
||||||
const isRecurent = key.includes("recurrent") && nbShipment;
|
const isRecurent = key.includes("recurrent") && nbShipment;
|
||||||
const productPrice = Number(product.price || product.price_kg);
|
const productPrice = Number(product.price || product.price_kg);
|
||||||
@@ -29,6 +29,12 @@ export function Contract() {
|
|||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { data: form } = getForm(Number(id), {enabled: !!id});
|
const { data: form } = getForm(Number(id), {enabled: !!id});
|
||||||
const inputForm = useForm<Record<string, number | string>>({
|
const inputForm = useForm<Record<string, number | string>>({
|
||||||
|
initialValues: {
|
||||||
|
firstname: "",
|
||||||
|
lastname: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
},
|
||||||
validate: {
|
validate: {
|
||||||
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null,
|
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null,
|
||||||
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null,
|
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null,
|
||||||
@@ -37,6 +43,8 @@ export function Contract() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createContractMutation = createContract();
|
||||||
|
|
||||||
const productsRecurent = useMemo(() => {
|
const productsRecurent = useMemo(() => {
|
||||||
return form?.productor?.products.filter((el) => el.type === "2")
|
return form?.productor?.products.filter((el) => el.type === "2")
|
||||||
}, [form]);
|
}, [form]);
|
||||||
@@ -50,10 +58,73 @@ export function Contract() {
|
|||||||
}, [form])
|
}, [form])
|
||||||
|
|
||||||
const price = useMemo(() => {
|
const price = useMemo(() => {
|
||||||
|
if (!allProducts) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const values = Object.entries(inputForm.getValues());
|
const values = Object.entries(inputForm.getValues());
|
||||||
return computePrices(values, allProducts, form?.shipments.length);
|
return computePrices(values, allProducts, form?.shipments.length);
|
||||||
}, [inputForm, allProducts, form?.shipments]);
|
}, [inputForm, allProducts, form?.shipments]);
|
||||||
|
|
||||||
|
const inputRefs: Record<string, React.RefObject<HTMLInputElement | null>> = {
|
||||||
|
firstname: useRef<HTMLInputElement>(null),
|
||||||
|
lastname: useRef<HTMLInputElement>(null),
|
||||||
|
email: useRef<HTMLInputElement>(null),
|
||||||
|
phone: useRef<HTMLInputElement>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShipmentsMinimumValue = useCallback(() => {
|
||||||
|
const shipmentErrors = form.shipments
|
||||||
|
.map((shipment) => {
|
||||||
|
const total = computePrices(
|
||||||
|
Object.entries(inputForm.getValues()),
|
||||||
|
shipment.products
|
||||||
|
);
|
||||||
|
if (total < (form?.minimum_shipment_value || 0)) {
|
||||||
|
return shipment.id; // mark shipment as invalid
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return shipmentErrors.length === 0;
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
const withDefaultValues = useCallback((values: Record<string, number | string>) => {
|
||||||
|
const result = {...values};
|
||||||
|
|
||||||
|
productsRecurent.forEach((product: Product) => {
|
||||||
|
const key = `recurrent-${product.id}`;
|
||||||
|
if (result[key] === undefined || result[key] === "") {
|
||||||
|
result[key] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.shipments.forEach((shipment) => {
|
||||||
|
shipment.products.forEach((product) => {
|
||||||
|
const key = `planned-${shipment.id}-${product.id}`;
|
||||||
|
if (result[key] === undefined || result[key] === "") {
|
||||||
|
result[key] = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [productsRecurent, form]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async () => {
|
||||||
|
const errors = inputForm.validate();
|
||||||
|
if (inputForm.isValid() && isShipmentsMinimumValue()) {
|
||||||
|
const contract = {
|
||||||
|
form_id: form.id,
|
||||||
|
contract: withDefaultValues(inputForm.getValues()),
|
||||||
|
}
|
||||||
|
await createContractMutation.mutateAsync(contract);
|
||||||
|
} else {
|
||||||
|
const firstErrorField = Object.keys(errors.errors)[0];
|
||||||
|
const ref = inputRefs[firstErrorField];
|
||||||
|
ref?.current?.scrollIntoView({behavior: "smooth", block: "center"});
|
||||||
|
}
|
||||||
|
}, [inputForm, inputRefs, isShipmentsMinimumValue, form]);
|
||||||
|
|
||||||
if (!form)
|
if (!form)
|
||||||
return <Loader/>;
|
return <Loader/>;
|
||||||
|
|
||||||
@@ -74,6 +145,7 @@ export function Contract() {
|
|||||||
required
|
required
|
||||||
leftSection={<IconUser/>}
|
leftSection={<IconUser/>}
|
||||||
{...inputForm.getInputProps('firstname')}
|
{...inputForm.getInputProps('firstname')}
|
||||||
|
ref={inputRefs.firstname}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("lastname", {capfirst: true})}
|
label={t("lastname", {capfirst: true})}
|
||||||
@@ -83,6 +155,7 @@ export function Contract() {
|
|||||||
required
|
required
|
||||||
leftSection={<IconUser/>}
|
leftSection={<IconUser/>}
|
||||||
{...inputForm.getInputProps('lastname')}
|
{...inputForm.getInputProps('lastname')}
|
||||||
|
ref={inputRefs.lastname}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
@@ -94,6 +167,7 @@ export function Contract() {
|
|||||||
required
|
required
|
||||||
leftSection={<IconMail/>}
|
leftSection={<IconMail/>}
|
||||||
{...inputForm.getInputProps('email')}
|
{...inputForm.getInputProps('email')}
|
||||||
|
ref={inputRefs.email}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t("phone", {capfirst: true})}
|
label={t("phone", {capfirst: true})}
|
||||||
@@ -103,6 +177,7 @@ export function Contract() {
|
|||||||
required
|
required
|
||||||
leftSection={<IconPhone/>}
|
leftSection={<IconPhone/>}
|
||||||
{...inputForm.getInputProps('phone')}
|
{...inputForm.getInputProps('phone')}
|
||||||
|
ref={inputRefs.phone}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
|
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
|
||||||
@@ -149,6 +224,7 @@ export function Contract() {
|
|||||||
{
|
{
|
||||||
shipments.map((shipment, index) => (
|
shipments.map((shipment, index) => (
|
||||||
<ShipmentForm
|
<ShipmentForm
|
||||||
|
minimumPrice={form.minimum_shipment_value}
|
||||||
shipment={shipment}
|
shipment={shipment}
|
||||||
index={index}
|
index={index}
|
||||||
inputForm={inputForm}
|
inputForm={inputForm}
|
||||||
@@ -180,10 +256,7 @@ export function Contract() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('submit contract')}
|
aria-label={t('submit contract')}
|
||||||
onClick={() => {
|
onClick={handleSubmit}
|
||||||
inputForm.validate();
|
|
||||||
console.log(inputForm.getValues())
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('submit contract')}
|
{t('submit contract')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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="templates">{t("templates", {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/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import FormModal from "@/components/Forms/Modal";
|
|||||||
import FormRow from "@/components/Forms/Row";
|
import FormRow from "@/components/Forms/Row";
|
||||||
import type { Form, FormInputs } from "@/services/resources/forms";
|
import type { Form, FormInputs } from "@/services/resources/forms";
|
||||||
import FilterForms from "@/components/Forms/Filter";
|
import FilterForms from "@/components/Forms/Filter";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
export function Forms() {
|
export function Forms() {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
@@ -52,11 +53,16 @@ export function Forms() {
|
|||||||
await createFormMutation.mutateAsync({
|
await createFormMutation.mutateAsync({
|
||||||
...form,
|
...form,
|
||||||
start: form?.start,
|
start: form?.start,
|
||||||
end: form?.start,
|
end: form?.end,
|
||||||
productor_id: Number(form.productor_id),
|
productor_id: Number(form.productor_id),
|
||||||
referer_id: Number(form.referer_id)
|
referer_id: Number(form.referer_id),
|
||||||
|
minimum_shipment_value: Number(form.minimum_shipment_value),
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created form", {capfirst: true}),
|
||||||
|
});
|
||||||
}, [createFormMutation]);
|
}, [createFormMutation]);
|
||||||
|
|
||||||
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
|
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
|
||||||
@@ -67,12 +73,17 @@ export function Forms() {
|
|||||||
form: {
|
form: {
|
||||||
...form,
|
...form,
|
||||||
start: form.start,
|
start: form.start,
|
||||||
end: form.start,
|
end: form.end,
|
||||||
productor_id: Number(form.productor_id),
|
productor_id: Number(form.productor_id),
|
||||||
referer_id: Number(form.referer_id)
|
referer_id: Number(form.referer_id),
|
||||||
|
minimum_shipment_value: Number(form.minimum_shipment_value),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited form", {capfirst: true}),
|
||||||
|
});
|
||||||
}, [editFormMutation]);
|
}, [editFormMutation]);
|
||||||
|
|
||||||
const onFilterChange = useCallback((
|
const onFilterChange = useCallback((
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Flex } from "@mantine/core";
|
import { Flex, Text } from "@mantine/core";
|
||||||
import { t } from "@/config/i18n";
|
|
||||||
import { useParams } from "react-router";
|
|
||||||
import { getForms } from "@/services/api";
|
import { getForms } from "@/services/api";
|
||||||
import { FormCard } from "@/components/Forms/Card";
|
import { FormCard } from "@/components/Forms/Card";
|
||||||
import type { Form } from "@/services/resources/forms";
|
import type { Form } from "@/services/resources/forms";
|
||||||
|
import { t } from "@/config/i18n";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const { data: allForms } = getForms();
|
const { data: allForms } = getForms();
|
||||||
@@ -11,9 +10,11 @@ export function Home() {
|
|||||||
return (
|
return (
|
||||||
<Flex gap="md" wrap="wrap" justify="center">
|
<Flex gap="md" wrap="wrap" justify="center">
|
||||||
{
|
{
|
||||||
allForms?.map((form: Form) => (
|
allForms && allForms?.length > 0 ?
|
||||||
<FormCard form={form} key={form.id}/>
|
allForms.map((form: Form) => (
|
||||||
))
|
<FormCard form={form} key={form.id}/>
|
||||||
|
)) :
|
||||||
|
<Text mt="lg" size="lg">{t("there is no contract for now",{capfirst: true})}</Text>
|
||||||
}
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
24
frontend/src/pages/NotFound/index.tsx
Normal file
24
frontend/src/pages/NotFound/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { t } from "@/config/i18n";
|
||||||
|
import { ActionIcon, Stack, Text, Title, Tooltip } from "@mantine/core";
|
||||||
|
import { IconHome } from "@tabler/icons-react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
export function NotFound() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
return (
|
||||||
|
<Stack justify="center" align="center">
|
||||||
|
<Title order={2}>{t("oops", {capfirst: true})}</Title>
|
||||||
|
<Text>{t('this page does not exists', {capfirst: true})}</Text>
|
||||||
|
<Tooltip label={t('back to home', {capfirst: true})}>
|
||||||
|
<ActionIcon
|
||||||
|
aria-label={t("back to home", {capfirst: true})}
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconHome/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { ProductorModal } from "@/components/Productors/Modal";
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import type { Productor, ProductorInputs } from "@/services/resources/productors";
|
import type { Productor, ProductorInputs } from "@/services/resources/productors";
|
||||||
import ProductorsFilters from "@/components/Productors/Filter";
|
import ProductorsFilters from "@/components/Productors/Filter";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
export default function Productors() {
|
export default function Productors() {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
@@ -50,6 +51,10 @@ export default function Productors() {
|
|||||||
...productor
|
...productor
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created productor", {capfirst: true}),
|
||||||
|
});
|
||||||
}, [createProductorMutation]);
|
}, [createProductorMutation]);
|
||||||
|
|
||||||
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
|
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
|
||||||
@@ -60,6 +65,10 @@ export default function Productors() {
|
|||||||
productor: productor
|
productor: productor
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited productor", {capfirst: true}),
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFilterChange = useCallback((values: string[], filter: string) => {
|
const onFilterChange = useCallback((values: string[], filter: string) => {
|
||||||
@@ -116,7 +125,7 @@ export default function Productors() {
|
|||||||
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("address", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("address", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("payment", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("payment methods", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ProductModal } from "@/components/Products/Modal";
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
|
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
|
||||||
import ProductsFilters from "@/components/Products/Filter";
|
import ProductsFilters from "@/components/Products/Filter";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
export default function Products() {
|
export default function Products() {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
@@ -48,6 +49,10 @@ export default function Products() {
|
|||||||
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
|
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
|
||||||
await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
|
await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created product", {capfirst: true}),
|
||||||
|
});
|
||||||
}, [createProductMutation]);
|
}, [createProductMutation]);
|
||||||
|
|
||||||
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
|
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
|
||||||
@@ -58,6 +63,10 @@ export default function Products() {
|
|||||||
product: productCreateFromProductInputs(product)
|
product: productCreateFromProductInputs(product)
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited product", {capfirst: true}),
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFilterChange = useCallback((values: string[], filter: string) => {
|
const onFilterChange = useCallback((values: string[], filter: string) => {
|
||||||
@@ -116,7 +125,6 @@ export default function Products() {
|
|||||||
<Table.Th>{t("price", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("price", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("quantity", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("quantity", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("quantity unit", {capfirst: true})}</Table.Th>
|
|
||||||
<Table.Th>{t("unit", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("unit", {capfirst: true})}</Table.Th>
|
||||||
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
|
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCallback, useMemo } from "react";
|
|||||||
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
|
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
|
||||||
import ShipmentModal from "@/components/Shipments/Modal";
|
import ShipmentModal from "@/components/Shipments/Modal";
|
||||||
import ShipmentsFilters from "@/components/Shipments/Filter";
|
import ShipmentsFilters from "@/components/Shipments/Filter";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
export default function Shipments() {
|
export default function Shipments() {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
@@ -43,6 +44,10 @@ export default function Shipments() {
|
|||||||
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
|
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
|
||||||
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
|
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created shipment", {capfirst: true}),
|
||||||
|
});
|
||||||
}, [createShipmentMutation]);
|
}, [createShipmentMutation]);
|
||||||
|
|
||||||
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
|
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
|
||||||
@@ -53,6 +58,10 @@ export default function Shipments() {
|
|||||||
shipment: shipmentCreateFromShipmentInputs(shipment)
|
shipment: shipmentCreateFromShipmentInputs(shipment)
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited shipment", {capfirst: true}),
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFilterChange = useCallback((values: string[], filter: string) => {
|
const onFilterChange = useCallback((values: string[], filter: string) => {
|
||||||
|
|||||||
@@ -42,18 +42,18 @@ export default function Users() {
|
|||||||
const editUserMutation = editUser();
|
const editUserMutation = editUser();
|
||||||
|
|
||||||
const handleCreateUser = useCallback(async (user: UserInputs) => {
|
const handleCreateUser = useCallback(async (user: UserInputs) => {
|
||||||
await createUserMutation.mutateAsync(user);
|
await createUserMutation.mutateAsync(user);
|
||||||
closeModal();
|
closeModal();
|
||||||
}, [createUserMutation]);
|
}, [createUserMutation]);
|
||||||
|
|
||||||
const handleEditUser = useCallback(async (user: UserInputs, id?: number) => {
|
const handleEditUser = useCallback(async (user: UserInputs, id?: number) => {
|
||||||
if (!id)
|
if (!id)
|
||||||
return;
|
return;
|
||||||
await editUserMutation.mutateAsync({
|
await editUserMutation.mutateAsync({
|
||||||
id: id,
|
id: id,
|
||||||
user: user
|
user: user
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFilterChange = useCallback((values: string[], filter: string) => {
|
const onFilterChange = useCallback((values: string[], filter: string) => {
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ import { Forms } from "@/pages/Forms";
|
|||||||
import Dashboard from "@/pages/Dashboard";
|
import Dashboard from "@/pages/Dashboard";
|
||||||
import Productors from "@/pages/Productors";
|
import Productors from "@/pages/Productors";
|
||||||
import Products from "@/pages/Products";
|
import Products from "@/pages/Products";
|
||||||
import Templates from "@/pages/Templates";
|
|
||||||
import Users from "@/pages/Users";
|
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 { CreateForms } from "@/pages/Forms/CreateForm";
|
// import { CreateForms } from "@/pages/Forms/CreateForm";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
Component: Root,
|
Component: Root,
|
||||||
// errorElement: <NotFound />,
|
errorElement: <NotFound />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, Component: Home },
|
{ index: true, Component: Home },
|
||||||
{ path: "/forms", Component: Forms },
|
{ path: "/forms", Component: Forms },
|
||||||
@@ -31,7 +31,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: "templates", Component: Templates },
|
||||||
{ 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 },
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import type { Shipment, ShipmentCreate, ShipmentEditPayload } from "@/services/r
|
|||||||
import type { Productor, ProductorCreate, ProductorEditPayload } from "@/services/resources/productors";
|
import type { Productor, ProductorCreate, ProductorEditPayload } 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 { notifications } from "@mantine/notifications";
|
||||||
|
import { t } from "@/config/i18n";
|
||||||
|
|
||||||
export function getShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
|
export function getShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
|
||||||
const queryString = filters?.toString()
|
const queryString = filters?.toString()
|
||||||
@@ -43,7 +46,18 @@ export function createShipment() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created shipment", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing shipment`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -62,7 +76,18 @@ export function editShipment() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited shipment", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing shipment`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -79,7 +104,18 @@ export function deleteShipment() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully deleted shipment", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error deleting shipment`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,7 +157,18 @@ export function createProductor() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created productor", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing productor`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -140,7 +187,18 @@ export function editProductor() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited productor", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing productor`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -157,7 +215,18 @@ export function deleteProductor() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully deleted productor", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
await queryClient.invalidateQueries({ queryKey: ['productors'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error deleting productor`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -216,7 +285,18 @@ export function deleteForm() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully deleted form", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['forms'] })
|
await queryClient.invalidateQueries({ queryKey: ['forms'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error deleting form`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -235,7 +315,18 @@ export function editForm() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited form", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['forms'] })
|
await queryClient.invalidateQueries({ queryKey: ['forms'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing form`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -277,7 +368,18 @@ export function createProduct() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created product", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing product`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -294,7 +396,18 @@ export function deleteProduct() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully deleted product", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error deleting product`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -313,7 +426,18 @@ export function editProduct() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited product", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
await queryClient.invalidateQueries({ queryKey: ['products'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing product`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -355,7 +479,18 @@ export function createUser() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully created user", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing user`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -372,7 +507,18 @@ export function deleteUser() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully deleted user", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error deleting user`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -391,7 +537,44 @@ export function editUser() {
|
|||||||
}).then((res) => res.json());
|
}).then((res) => res.json());
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("success", {capfirst: true}),
|
||||||
|
message: t("successfully edited user", {capfirst: true}),
|
||||||
|
});
|
||||||
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
await queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
notifications.show({
|
||||||
|
title: t("error", {capfirst: true}),
|
||||||
|
message: error?.message || t(`error editing user`, {capfirst: true}),
|
||||||
|
color: "red"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createContract() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (newContract: ContractCreate) => {
|
||||||
|
return fetch(`${Config.backend_uri}/contracts`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newContract),
|
||||||
|
}).then(async (res) => await res.blob());
|
||||||
|
},
|
||||||
|
onSuccess: async (pdfBlob) => {
|
||||||
|
const url = URL.createObjectURL(pdfBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = `contract.pdf`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
4
frontend/src/services/resources/contracts.ts
Normal file
4
frontend/src/services/resources/contracts.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type ContractCreate = {
|
||||||
|
form_id: number;
|
||||||
|
contract: Record<string, string | number | null>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Productor } from "@/services/resources/productors";
|
import type { Productor } from "@/services/resources/productors";
|
||||||
import type { Shipment, ShipmentInputs } from "@/services/resources/shipments";
|
import type { Shipment } from "@/services/resources/shipments";
|
||||||
import type { User } from "@/services/resources/users";
|
import type { User } from "@/services/resources/users";
|
||||||
|
|
||||||
export type Form = {
|
export type Form = {
|
||||||
@@ -11,6 +11,7 @@ export type Form = {
|
|||||||
productor: Productor;
|
productor: Productor;
|
||||||
referer: User;
|
referer: User;
|
||||||
shipments: Shipment[];
|
shipments: Shipment[];
|
||||||
|
minimum_shipment_value: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormCreate = {
|
export type FormCreate = {
|
||||||
@@ -20,6 +21,7 @@ export type FormCreate = {
|
|||||||
end: string;
|
end: string;
|
||||||
productor_id: number;
|
productor_id: number;
|
||||||
referer_id: number;
|
referer_id: number;
|
||||||
|
minimum_shipment_value: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormEdit = {
|
export type FormEdit = {
|
||||||
@@ -29,6 +31,7 @@ export type FormEdit = {
|
|||||||
end?: string | null;
|
end?: string | null;
|
||||||
productor_id?: number | null;
|
productor_id?: number | null;
|
||||||
referer_id?: number | null;
|
referer_id?: number | null;
|
||||||
|
minimum_shipment_value: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormEditPayload = {
|
export type FormEditPayload = {
|
||||||
@@ -43,4 +46,5 @@ export type FormInputs = {
|
|||||||
end: string | null;
|
end: string | null;
|
||||||
productor_id: string;
|
productor_id: string;
|
||||||
referer_id: string;
|
referer_id: string;
|
||||||
|
minimum_shipment_value: number | string | null;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
|
import { t } from "@/config/i18n";
|
||||||
import type { Product } from "./products";
|
import type { Product } from "./products";
|
||||||
|
|
||||||
|
export const PaymentMethods = [
|
||||||
|
{value: "cheque", label: t("cheque", {capfirst: true})},
|
||||||
|
{value: "transfer", label: t("transfer", {capfirst: true})},
|
||||||
|
]
|
||||||
|
|
||||||
|
export type PaymentMethod = {
|
||||||
|
name: string;
|
||||||
|
details: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Productor = {
|
export type Productor = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
payment: string;
|
payment_methods: PaymentMethod[];
|
||||||
type: string;
|
type: string;
|
||||||
products: Product[]
|
products: Product[]
|
||||||
}
|
}
|
||||||
@@ -12,22 +23,22 @@ export type Productor = {
|
|||||||
export type ProductorCreate = {
|
export type ProductorCreate = {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
payment: string;
|
payment_methods: PaymentMethod[];
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductorEdit = {
|
export type ProductorEdit = {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
address: string | null;
|
address: string | null;
|
||||||
payment: string | null;
|
payment_methods: PaymentMethod[];
|
||||||
type: string | null;
|
type: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductorInputs = {
|
export type ProductorInputs = {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
payment: string;
|
|
||||||
type: string;
|
type: string;
|
||||||
|
payment_methods: PaymentMethod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductorEditPayload = {
|
export type ProductorEditPayload = {
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export type ProductInputs = {
|
|||||||
productor_id: string | null;
|
productor_id: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
unit: string | null;
|
unit: string | null;
|
||||||
price: number | null;
|
price: number | string | null;
|
||||||
price_kg: number | null;
|
price_kg: number | string | null;
|
||||||
quantity: number | null;
|
quantity: number | string | null;
|
||||||
quantity_unit: string | null;
|
quantity_unit: string | null;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
}
|
}
|
||||||
@@ -91,9 +91,9 @@ export function productCreateFromProductInputs(productInput: ProductInputs): Pro
|
|||||||
productor_id: Number(productInput.productor_id)!,
|
productor_id: Number(productInput.productor_id)!,
|
||||||
name: productInput.name,
|
name: productInput.name,
|
||||||
unit: productInput.unit!,
|
unit: productInput.unit!,
|
||||||
price: productInput.price!,
|
price: productInput.price === "" || !productInput.price ? null : Number(productInput.price),
|
||||||
price_kg: productInput.price_kg,
|
price_kg: productInput.price_kg === "" || !productInput.price_kg ? null : Number(productInput.price_kg),
|
||||||
quantity: productInput.quantity,
|
quantity: productInput.quantity === "" || !productInput.quantity ? null : Number(productInput.quantity),
|
||||||
quantity_unit: productInput.quantity_unit,
|
quantity_unit: productInput.quantity_unit,
|
||||||
type: productInput.type!,
|
type: productInput.type!,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user