add contract pdf generation
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
## Installation
|
||||
|
||||
```console
|
||||
pip install backend
|
||||
apt install weasyprint
|
||||
hatch shell
|
||||
fastapi dev src/main.py
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -27,7 +27,8 @@ dependencies = [
|
||||
"psycopg2-binary",
|
||||
"PyJWT",
|
||||
"cryptography",
|
||||
"requests"
|
||||
"requests",
|
||||
"weasyprint",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -13,6 +13,8 @@ import jwt
|
||||
from jwt import PyJWKClient
|
||||
import requests
|
||||
|
||||
from src.messages import tokenExpired, invalidToken
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
|
||||
jwk_client = PyJWKClient(JWKS_URL)
|
||||
@@ -78,9 +80,9 @@ def verify_token(token: str):
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(status_code=401, detail="Token expired")
|
||||
raise HTTPException(status_code=401, detail=tokenExpired)
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
raise HTTPException(status_code=401, detail=invalidToken)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
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."
|
||||
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):
|
||||
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):
|
||||
name: str
|
||||
address: str
|
||||
payment: str
|
||||
type: str
|
||||
|
||||
class ProductorPublic(ProductorBase):
|
||||
id: int
|
||||
products: list["Product"] = []
|
||||
payment_methods: list["PaymentMethod"] = []
|
||||
|
||||
class Productor(ProductorBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
@@ -39,15 +54,19 @@ class Productor(ProductorBase, table=True):
|
||||
"order_by": "Product.name"
|
||||
},
|
||||
)
|
||||
payment_methods: list["PaymentMethod"] = Relationship(
|
||||
back_populates="productor",
|
||||
cascade_delete=True
|
||||
)
|
||||
|
||||
class ProductorUpdate(SQLModel):
|
||||
name: str | None
|
||||
address: str | None
|
||||
payment: str | None
|
||||
payment_methods: list["PaymentMethod"] = []
|
||||
type: str | None
|
||||
|
||||
class ProductorCreate(ProductorBase):
|
||||
pass
|
||||
payment_methods: list["PaymentMethod"] = []
|
||||
|
||||
class Unit(StrEnum):
|
||||
GRAMS = "1"
|
||||
@@ -102,6 +121,7 @@ class FormBase(SQLModel):
|
||||
season: str
|
||||
start: datetime.date
|
||||
end: datetime.date
|
||||
minimum_shipment_value: float | None
|
||||
|
||||
class FormPublic(FormBase):
|
||||
id: int
|
||||
@@ -128,6 +148,7 @@ class FormUpdate(SQLModel):
|
||||
season: str | None
|
||||
start: datetime.date | None
|
||||
end: datetime.date | None
|
||||
minimum_shipment_value: float | None
|
||||
|
||||
class FormCreate(FormBase):
|
||||
pass
|
||||
@@ -148,13 +169,14 @@ class TemplateCreate(TemplateBase):
|
||||
pass
|
||||
|
||||
class ContractBase(SQLModel):
|
||||
pass
|
||||
form_id: int
|
||||
contract: dict
|
||||
|
||||
class ContractPublic(ContractBase):
|
||||
id: int
|
||||
|
||||
class Contract(ContractBase, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
# class Contract(ContractBase, table=True):
|
||||
# id: int | None = Field(default=None, primary_key=True)
|
||||
|
||||
class ContractUpdate(SQLModel):
|
||||
pass
|
||||
|
||||
@@ -13,8 +13,16 @@ def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
|
||||
return session.get(models.Productor, productor_id)
|
||||
|
||||
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.payment_methods = [
|
||||
models.PaymentMethod(
|
||||
name=pm.name,
|
||||
details=pm.details
|
||||
) for pm in productor.payment_methods
|
||||
]
|
||||
|
||||
session.add(new_productor)
|
||||
session.commit()
|
||||
session.refresh(new_productor)
|
||||
@@ -26,7 +34,20 @@ def update_one(session: Session, id: int, productor: models.ProductorUpdate) ->
|
||||
new_productor = result.first()
|
||||
if not new_productor:
|
||||
return None
|
||||
|
||||
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():
|
||||
setattr(new_productor, key, value)
|
||||
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 type": "filter by type",
|
||||
"address": "address",
|
||||
"payment": "payment",
|
||||
"payment methods": "payment methods",
|
||||
"cheque": "cheque",
|
||||
"transfer": "transfer",
|
||||
"type": "type",
|
||||
"create productor": "create productor",
|
||||
"productor name": "productor name",
|
||||
@@ -61,6 +63,13 @@
|
||||
"there is": "there is",
|
||||
"for this contract": "for this contact.",
|
||||
"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",
|
||||
"productors": "productors",
|
||||
"products": "products",
|
||||
@@ -83,5 +92,34 @@
|
||||
"a lastname": "a lastname",
|
||||
"a email": "a email",
|
||||
"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."
|
||||
}
|
||||
@@ -2,17 +2,16 @@
|
||||
"product name": "nom du produit",
|
||||
"product price": "prix du produit",
|
||||
"product quantity": "quantité du produit",
|
||||
"product quantity unit": "Unité de quantité du produit",
|
||||
"product type": "type de produit",
|
||||
"planned": "planifié",
|
||||
"planned products": "Produits planifiés par livraison",
|
||||
"select products per shipment": "Selectionnez les produits pour chaque livraison.",
|
||||
"recurrent": "récurrent",
|
||||
"recurrent": "récurent",
|
||||
"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 unit": "unité de vente du produit",
|
||||
"grams": "grammes",
|
||||
"kilo": "kilo",
|
||||
"piece": "pièce",
|
||||
"in": "en",
|
||||
"enter quantity": "entrez la quantitée",
|
||||
@@ -33,20 +32,24 @@
|
||||
"number of shipment": "nombre de livraisons",
|
||||
"cancel": "annuler",
|
||||
"create form": "créer un formulare de contrat",
|
||||
"edit productor": "modifier le producteur·trice",
|
||||
"remove productor": "supprimer le producteur·trice",
|
||||
"create productor": "créer le/la producteur·trice",
|
||||
"edit productor": "modifier le/la producteur·trice",
|
||||
"remove productor": "supprimer le/la producteur·trice",
|
||||
"home": "accueil",
|
||||
"dashboard": "tableau de bord",
|
||||
"filter by name": "filtrer par nom",
|
||||
"filter by type": "filtrer par type",
|
||||
"address": "adresse",
|
||||
"payment": "ordre du chèque",
|
||||
"payment methods": "méthodes de paiement",
|
||||
"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 type": "type 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",
|
||||
"quantity": "quantité",
|
||||
"quantity unit": "unité de quantité",
|
||||
@@ -60,6 +63,13 @@
|
||||
"shipment date": "date de la livraison",
|
||||
"shipments": "livraisons",
|
||||
"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",
|
||||
"for this contract": "pour ce contrat.",
|
||||
"remove shipment": "supprimer la livraison",
|
||||
@@ -79,11 +89,50 @@
|
||||
"a start date": "une date de début",
|
||||
"a end date": "une date de fin",
|
||||
"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 fistname": "un prénom",
|
||||
"a lastname": "un nom",
|
||||
"a email": "une adresse email",
|
||||
"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."
|
||||
}
|
||||
92
frontend/package-lock.json
generated
92
frontend/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@mantine/dates": "^8.3.14",
|
||||
"@mantine/form": "^8.3.14",
|
||||
"@mantine/hooks": "^8.3.14",
|
||||
"@mantine/notifications": "^8.3.14",
|
||||
"@tabler/icons": "^3.36.1",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
@@ -1147,6 +1148,31 @@
|
||||
"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": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@@ -2226,7 +2252,6 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
@@ -2266,6 +2291,16 @@
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"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": {
|
||||
"version": "1.5.286",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
||||
@@ -2818,7 +2853,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -2937,6 +2971,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -3009,6 +3055,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -3277,6 +3332,17 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"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": {
|
||||
"version": "5.4.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@mantine/dates": "^8.3.14",
|
||||
"@mantine/form": "^8.3.14",
|
||||
"@mantine/hooks": "^8.3.14",
|
||||
"@mantine/notifications": "^8.3.14",
|
||||
"@tabler/icons": "^3.36.1",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
|
||||
@@ -16,6 +16,7 @@ export function FormCard({form}: FormCardProps) {
|
||||
<Box
|
||||
component={Link}
|
||||
to={`/form/${form.id}`}
|
||||
style={{textDecoration: "none", color: "black"}}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<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 { 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 { useForm } from "@mantine/form";
|
||||
import { useEffect, useMemo } from "react";
|
||||
@@ -20,6 +20,7 @@ export default function FormModal({
|
||||
}: FormModalProps) {
|
||||
const {data: productors} = getProductors();
|
||||
const {data: users} = getUsers();
|
||||
|
||||
const form = useForm<FormInputs>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
@@ -28,6 +29,7 @@ export default function FormModal({
|
||||
end: null,
|
||||
productor_id: "",
|
||||
referer_id: "",
|
||||
minimum_shipment_value: null,
|
||||
},
|
||||
validate: {
|
||||
name: (value) =>
|
||||
@@ -67,10 +69,9 @@ export default function FormModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={currentForm ? t("edit form") : t('create form')}
|
||||
title={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||
>
|
||||
<TextInput
|
||||
label={t("form name", {capfirst: true})}
|
||||
@@ -123,6 +124,13 @@ export default function FormModal({
|
||||
data={productorsSelect || []}
|
||||
{...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">
|
||||
<Button
|
||||
variant="filled"
|
||||
@@ -137,6 +145,7 @@ export default function FormModal({
|
||||
<Button
|
||||
variant="filled"
|
||||
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||
leftSection={currentForm ? <IconEdit/> : <IconPlus/>}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-self: left;
|
||||
width: 50%;
|
||||
padding: 1rem;
|
||||
background-color: var(--mantine-color-blue-4);
|
||||
}
|
||||
|
||||
a {
|
||||
gap: 1em;
|
||||
.navLink {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,18 @@ import "./index.css";
|
||||
export function Navbar() {
|
||||
return (
|
||||
<nav>
|
||||
<NavLink to="/">{t("home", {capfirst: true})}</NavLink>
|
||||
<NavLink to="/dashboard/productors">{t("dashboard", {capfirst: true})}</NavLink>
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
to="/"
|
||||
>
|
||||
{t("home", {capfirst: true})}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
to="/dashboard/productors"
|
||||
>
|
||||
{t("dashboard", {capfirst: true})}
|
||||
</NavLink>
|
||||
</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 { useForm } from "@mantine/form";
|
||||
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";
|
||||
|
||||
export type ProductorModalProps = ModalBaseProps & {
|
||||
@@ -20,18 +20,16 @@ export function ProductorModal({
|
||||
initialValues: {
|
||||
name: "",
|
||||
address: "",
|
||||
payment: "",
|
||||
payment_methods: [],
|
||||
type: "",
|
||||
},
|
||||
validate: {
|
||||
name: (value) =>
|
||||
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
|
||||
!value ? `${t("name", {capfirst: true})} ${t("is required")}` : null,
|
||||
address: (value) =>
|
||||
!value ? `${t("address", {capfirst: true})} ${t('is required')}` : null,
|
||||
payment: (value) =>
|
||||
!value ? `${t("payment", {capfirst: true})} ${t('is required')}` : null,
|
||||
!value ? `${t("address", {capfirst: true})} ${t("is required")}` : null,
|
||||
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 (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create productor", {capfirst: true})}
|
||||
@@ -56,30 +53,65 @@ export function ProductorModal({
|
||||
placeholder={t("productor name", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor type", {capfirst: true})}
|
||||
placeholder={t("productor type", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('type')}
|
||||
{...form.getInputProps("type")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor address", {capfirst: true})}
|
||||
placeholder={t("productor address", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('address')}
|
||||
{...form.getInputProps("address")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor payment", {capfirst: true})}
|
||||
placeholder={t("productor payment", {capfirst: true})}
|
||||
<MultiSelect
|
||||
label={t("payment methods", {capfirst: true})}
|
||||
placeholder={t("payment methods", {capfirst: true})}
|
||||
radius="sm"
|
||||
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">
|
||||
<Button
|
||||
variant="filled"
|
||||
@@ -93,14 +125,15 @@ export function ProductorModal({
|
||||
>{t("cancel", {capfirst: true})}</Button>
|
||||
<Button
|
||||
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={() => {
|
||||
form.validate();
|
||||
console.log(form.getValues())
|
||||
if (form.isValid()) {
|
||||
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>
|
||||
</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 { IconEdit, IconX } from "@tabler/icons-react";
|
||||
import type { Productor } from "@/services/resources/productors";
|
||||
@@ -21,7 +21,15 @@ export default function ProductorRow({
|
||||
<Table.Td>{productor.name}</Table.Td>
|
||||
<Table.Td>{productor.type}</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>
|
||||
<Tooltip label={t("edit productor", {capfirst: true})}>
|
||||
<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 { 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 { useEffect, useMemo } from "react";
|
||||
import { getProductors } from "@/services/api";
|
||||
import { InputLabel } from "@/components/Label";
|
||||
|
||||
export type ProductModalProps = ModalBaseProps & {
|
||||
currentProduct?: Product;
|
||||
@@ -42,7 +43,7 @@ export function ProductModal({
|
||||
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
|
||||
productor_id: (value) =>
|
||||
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,7 +58,6 @@ export function ProductModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create product", {capfirst: true})}
|
||||
@@ -78,19 +78,23 @@ export function ProductModal({
|
||||
label={t("product name", {capfirst: true})}
|
||||
placeholder={t("product name", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Select
|
||||
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})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
searchable
|
||||
clearable
|
||||
data={[
|
||||
{value: "1", label: t("planned")},
|
||||
{value: "2", label: t("recurrent")}
|
||||
{value: "1", label: t("planned", {capfirst: true})},
|
||||
{value: "2", label: t("recurrent", {capfirst: true})}
|
||||
]}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
@@ -103,7 +107,7 @@ export function ProductModal({
|
||||
withAsterisk
|
||||
searchable
|
||||
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')}
|
||||
/>
|
||||
<Group grow>
|
||||
@@ -131,7 +135,9 @@ export function ProductModal({
|
||||
label={t("product quantity unit", {capfirst: true})}
|
||||
placeholder={t("product quantity unit", {capfirst: true})}
|
||||
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})}
|
||||
/>
|
||||
|
||||
@@ -152,7 +158,6 @@ export function ProductModal({
|
||||
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
console.log(form.isValid(), form.getValues())
|
||||
if (form.isValid()) {
|
||||
handleSubmit(form.getValues(), currentProduct?.id)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,27 @@ export default function ProductRow({
|
||||
<Table.Tr key={product.id}>
|
||||
<Table.Td>{product.name}</Table.Td>
|
||||
<Table.Td>{t(ProductType[product.type])}</Table.Td>
|
||||
<Table.Td>{product.price}</Table.Td>
|
||||
<Table.Td>{product.price_kg}</Table.Td>
|
||||
<Table.Td>{product.quantity}</Table.Td>
|
||||
<Table.Td>{product.quantity_unit}</Table.Td>
|
||||
<Table.Td>{t(ProductUnit[product.unit])}</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
product.price ?
|
||||
Intl.NumberFormat(
|
||||
"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>
|
||||
<Tooltip label={t("edit product", {capfirst: true})}>
|
||||
<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 { ProductForm } from "@/components/Products/Form";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
import { useMemo } from "react";
|
||||
import { computePrices } from "@/pages/Contract";
|
||||
import { t } from "@/config/i18n";
|
||||
|
||||
export type ShipmentFormProps = {
|
||||
inputForm: UseFormReturnType<Record<string, string | number>>;
|
||||
shipment: Shipment;
|
||||
minimumPrice?: number | null;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -15,6 +17,7 @@ export default function ShipmentForm({
|
||||
shipment,
|
||||
index,
|
||||
inputForm,
|
||||
minimumPrice,
|
||||
}: ShipmentFormProps) {
|
||||
const shipmentPrice = useMemo(() => {
|
||||
const values = Object
|
||||
@@ -26,18 +29,42 @@ export default function ShipmentForm({
|
||||
return computePrices(values, shipment.products);
|
||||
}, [inputForm, shipment.products]);
|
||||
|
||||
const priceRequirement = useMemo(() => {
|
||||
if (!minimumPrice)
|
||||
return false;
|
||||
return minimumPrice ? shipmentPrice < minimumPrice : true
|
||||
}, [shipmentPrice, minimumPrice])
|
||||
|
||||
return (
|
||||
<Accordion.Item value={String(index)}>
|
||||
<Accordion.Control>
|
||||
<Group justify="space-between">
|
||||
<Text>{shipment.name}</Text>
|
||||
<Text>{
|
||||
<Stack gap={0}>
|
||||
<Text c={priceRequirement ? "red" : "green"}>{
|
||||
Intl.NumberFormat(
|
||||
"fr-FR",
|
||||
{style: "currency", currency: "EUR"}
|
||||
).format(shipmentPrice)
|
||||
}</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>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
|
||||
@@ -63,7 +63,6 @@ export default function ShipmentModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={currentShipment ? t("edit shipment") : t('create shipment')}
|
||||
@@ -93,6 +92,7 @@ export default function ShipmentModal({
|
||||
<MultiSelect
|
||||
label={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 || []}
|
||||
clearable
|
||||
searchable
|
||||
|
||||
@@ -37,7 +37,6 @@ export function UserModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create user", {capfirst: true})}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { MantineProvider } from "@mantine/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@@ -13,6 +15,7 @@ createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider>
|
||||
<Notifications />
|
||||
<RouterProvider router={router} />
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ProductForm } from "@/components/Products/Form";
|
||||
import ShipmentForm from "@/components/Shipments/Form";
|
||||
import { t } from "@/config/i18n";
|
||||
import { getForm } from "@/services/api";
|
||||
import { createContract, getForm } from "@/services/api";
|
||||
import { type Product } from "@/services/resources/products";
|
||||
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
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 product = products.find((product) => product.id === productId);
|
||||
if (!product) {
|
||||
return 0;
|
||||
return prev + 0;
|
||||
}
|
||||
const isRecurent = key.includes("recurrent") && nbShipment;
|
||||
const productPrice = Number(product.price || product.price_kg);
|
||||
@@ -29,6 +29,12 @@ export function Contract() {
|
||||
const { id } = useParams();
|
||||
const { data: form } = getForm(Number(id), {enabled: !!id});
|
||||
const inputForm = useForm<Record<string, number | string>>({
|
||||
initialValues: {
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
},
|
||||
validate: {
|
||||
firstname: (value) => !value ? `${t("a firstname", {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(() => {
|
||||
return form?.productor?.products.filter((el) => el.type === "2")
|
||||
}, [form]);
|
||||
@@ -50,10 +58,73 @@ export function Contract() {
|
||||
}, [form])
|
||||
|
||||
const price = useMemo(() => {
|
||||
if (!allProducts) {
|
||||
return 0;
|
||||
}
|
||||
const values = Object.entries(inputForm.getValues());
|
||||
return computePrices(values, allProducts, form?.shipments.length);
|
||||
}, [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)
|
||||
return <Loader/>;
|
||||
|
||||
@@ -74,6 +145,7 @@ export function Contract() {
|
||||
required
|
||||
leftSection={<IconUser/>}
|
||||
{...inputForm.getInputProps('firstname')}
|
||||
ref={inputRefs.firstname}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("lastname", {capfirst: true})}
|
||||
@@ -83,6 +155,7 @@ export function Contract() {
|
||||
required
|
||||
leftSection={<IconUser/>}
|
||||
{...inputForm.getInputProps('lastname')}
|
||||
ref={inputRefs.lastname}
|
||||
/>
|
||||
</Group>
|
||||
<Group grow>
|
||||
@@ -94,6 +167,7 @@ export function Contract() {
|
||||
required
|
||||
leftSection={<IconMail/>}
|
||||
{...inputForm.getInputProps('email')}
|
||||
ref={inputRefs.email}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("phone", {capfirst: true})}
|
||||
@@ -103,6 +177,7 @@ export function Contract() {
|
||||
required
|
||||
leftSection={<IconPhone/>}
|
||||
{...inputForm.getInputProps('phone')}
|
||||
ref={inputRefs.phone}
|
||||
/>
|
||||
</Group>
|
||||
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
|
||||
@@ -149,6 +224,7 @@ export function Contract() {
|
||||
{
|
||||
shipments.map((shipment, index) => (
|
||||
<ShipmentForm
|
||||
minimumPrice={form.minimum_shipment_value}
|
||||
shipment={shipment}
|
||||
index={index}
|
||||
inputForm={inputForm}
|
||||
@@ -180,10 +256,7 @@ export function Contract() {
|
||||
</Text>
|
||||
<Button
|
||||
aria-label={t('submit contract')}
|
||||
onClick={() => {
|
||||
inputForm.validate();
|
||||
console.log(inputForm.getValues())
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t('submit contract')}
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Dashboard() {
|
||||
<Tabs.Tab value="products">{t("products", {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="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.List>
|
||||
<Outlet/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import FormModal from "@/components/Forms/Modal";
|
||||
import FormRow from "@/components/Forms/Row";
|
||||
import type { Form, FormInputs } from "@/services/resources/forms";
|
||||
import FilterForms from "@/components/Forms/Filter";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export function Forms() {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
@@ -52,11 +53,16 @@ export function Forms() {
|
||||
await createFormMutation.mutateAsync({
|
||||
...form,
|
||||
start: form?.start,
|
||||
end: form?.start,
|
||||
end: form?.end,
|
||||
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();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created form", {capfirst: true}),
|
||||
});
|
||||
}, [createFormMutation]);
|
||||
|
||||
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
|
||||
@@ -67,12 +73,17 @@ export function Forms() {
|
||||
form: {
|
||||
...form,
|
||||
start: form.start,
|
||||
end: form.start,
|
||||
end: form.end,
|
||||
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();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited form", {capfirst: true}),
|
||||
});
|
||||
}, [editFormMutation]);
|
||||
|
||||
const onFilterChange = useCallback((
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Flex } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useParams } from "react-router";
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import { getForms } from "@/services/api";
|
||||
import { FormCard } from "@/components/Forms/Card";
|
||||
import type { Form } from "@/services/resources/forms";
|
||||
import { t } from "@/config/i18n";
|
||||
|
||||
export function Home() {
|
||||
const { data: allForms } = getForms();
|
||||
@@ -11,9 +10,11 @@ export function Home() {
|
||||
return (
|
||||
<Flex gap="md" wrap="wrap" justify="center">
|
||||
{
|
||||
allForms?.map((form: Form) => (
|
||||
allForms && allForms?.length > 0 ?
|
||||
allForms.map((form: Form) => (
|
||||
<FormCard form={form} key={form.id}/>
|
||||
))
|
||||
)) :
|
||||
<Text mt="lg" size="lg">{t("there is no contract for now",{capfirst: true})}</Text>
|
||||
}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
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 type { Productor, ProductorInputs } from "@/services/resources/productors";
|
||||
import ProductorsFilters from "@/components/Productors/Filter";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function Productors() {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
@@ -50,6 +51,10 @@ export default function Productors() {
|
||||
...productor
|
||||
});
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created productor", {capfirst: true}),
|
||||
});
|
||||
}, [createProductorMutation]);
|
||||
|
||||
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
|
||||
@@ -60,6 +65,10 @@ export default function Productors() {
|
||||
productor: productor
|
||||
});
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited productor", {capfirst: true}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
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("type", {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.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ProductModal } from "@/components/Products/Modal";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
|
||||
import ProductsFilters from "@/components/Products/Filter";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function Products() {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
@@ -48,6 +49,10 @@ export default function Products() {
|
||||
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
|
||||
await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created product", {capfirst: true}),
|
||||
});
|
||||
}, [createProductMutation]);
|
||||
|
||||
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
|
||||
@@ -58,6 +63,10 @@ export default function Products() {
|
||||
product: productCreateFromProductInputs(product)
|
||||
});
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited product", {capfirst: true}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
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("priceKg", {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("actions", {capfirst: true})}</Table.Th>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useCallback, useMemo } from "react";
|
||||
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
|
||||
import ShipmentModal from "@/components/Shipments/Modal";
|
||||
import ShipmentsFilters from "@/components/Shipments/Filter";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function Shipments() {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
@@ -43,6 +44,10 @@ export default function Shipments() {
|
||||
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
|
||||
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created shipment", {capfirst: true}),
|
||||
});
|
||||
}, [createShipmentMutation]);
|
||||
|
||||
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
|
||||
@@ -53,6 +58,10 @@ export default function Shipments() {
|
||||
shipment: shipmentCreateFromShipmentInputs(shipment)
|
||||
});
|
||||
closeModal();
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited shipment", {capfirst: true}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFilterChange = useCallback((values: string[], filter: string) => {
|
||||
|
||||
@@ -8,17 +8,17 @@ import { Forms } from "@/pages/Forms";
|
||||
import Dashboard from "@/pages/Dashboard";
|
||||
import Productors from "@/pages/Productors";
|
||||
import Products from "@/pages/Products";
|
||||
import Templates from "@/pages/Templates";
|
||||
import Users from "@/pages/Users";
|
||||
import Shipments from "./pages/Shipments";
|
||||
import { Contract } from "./pages/Contract";
|
||||
import { NotFound } from "./pages/NotFound";
|
||||
// import { CreateForms } from "@/pages/Forms/CreateForm";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: Root,
|
||||
// errorElement: <NotFound />,
|
||||
errorElement: <NotFound />,
|
||||
children: [
|
||||
{ index: true, Component: Home },
|
||||
{ path: "/forms", Component: Forms },
|
||||
@@ -31,7 +31,7 @@ export const router = createBrowserRouter([
|
||||
{ path: "products", Component: Products },
|
||||
{ path: "products/create", Component: Products },
|
||||
{ path: "products/:id/edit", Component: Products },
|
||||
{ path: "templates", Component: Templates },
|
||||
// { path: "templates", Component: Templates },
|
||||
{ path: "users", Component: Users },
|
||||
{ path: "users/create", 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 { User, UserCreate, UserEditPayload } from "@/services/resources/users";
|
||||
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> {
|
||||
const queryString = filters?.toString()
|
||||
@@ -43,7 +46,18 @@ export function createShipment() {
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created shipment", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited shipment", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully deleted shipment", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created productor", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited productor", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully deleted productor", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully deleted form", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited form", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created product", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully deleted product", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited product", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully created user", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully deleted user", {capfirst: true}),
|
||||
});
|
||||
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());
|
||||
},
|
||||
onSuccess: async () => {
|
||||
notifications.show({
|
||||
title: t("success", {capfirst: true}),
|
||||
message: t("successfully edited user", {capfirst: true}),
|
||||
});
|
||||
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 { Shipment, ShipmentInputs } from "@/services/resources/shipments";
|
||||
import type { Shipment } from "@/services/resources/shipments";
|
||||
import type { User } from "@/services/resources/users";
|
||||
|
||||
export type Form = {
|
||||
@@ -11,6 +11,7 @@ export type Form = {
|
||||
productor: Productor;
|
||||
referer: User;
|
||||
shipments: Shipment[];
|
||||
minimum_shipment_value: number | null;
|
||||
}
|
||||
|
||||
export type FormCreate = {
|
||||
@@ -20,6 +21,7 @@ export type FormCreate = {
|
||||
end: string;
|
||||
productor_id: number;
|
||||
referer_id: number;
|
||||
minimum_shipment_value: number | null;
|
||||
}
|
||||
|
||||
export type FormEdit = {
|
||||
@@ -29,6 +31,7 @@ export type FormEdit = {
|
||||
end?: string | null;
|
||||
productor_id?: number | null;
|
||||
referer_id?: number | null;
|
||||
minimum_shipment_value: number | null;
|
||||
}
|
||||
|
||||
export type FormEditPayload = {
|
||||
@@ -43,4 +46,5 @@ export type FormInputs = {
|
||||
end: string | null;
|
||||
productor_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";
|
||||
|
||||
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 = {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
payment: string;
|
||||
payment_methods: PaymentMethod[];
|
||||
type: string;
|
||||
products: Product[]
|
||||
}
|
||||
@@ -12,22 +23,22 @@ export type Productor = {
|
||||
export type ProductorCreate = {
|
||||
name: string;
|
||||
address: string;
|
||||
payment: string;
|
||||
payment_methods: PaymentMethod[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type ProductorEdit = {
|
||||
name: string | null;
|
||||
address: string | null;
|
||||
payment: string | null;
|
||||
payment_methods: PaymentMethod[];
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
export type ProductorInputs = {
|
||||
name: string;
|
||||
address: string;
|
||||
payment: string;
|
||||
type: string;
|
||||
payment_methods: PaymentMethod[];
|
||||
}
|
||||
|
||||
export type ProductorEditPayload = {
|
||||
|
||||
@@ -61,9 +61,9 @@ export type ProductInputs = {
|
||||
productor_id: string | null;
|
||||
name: string;
|
||||
unit: string | null;
|
||||
price: number | null;
|
||||
price_kg: number | null;
|
||||
quantity: number | null;
|
||||
price: number | string | null;
|
||||
price_kg: number | string | null;
|
||||
quantity: number | string | null;
|
||||
quantity_unit: string | null;
|
||||
type: string | null;
|
||||
}
|
||||
@@ -91,9 +91,9 @@ export function productCreateFromProductInputs(productInput: ProductInputs): Pro
|
||||
productor_id: Number(productInput.productor_id)!,
|
||||
name: productInput.name,
|
||||
unit: productInput.unit!,
|
||||
price: productInput.price!,
|
||||
price_kg: productInput.price_kg,
|
||||
quantity: productInput.quantity,
|
||||
price: productInput.price === "" || !productInput.price ? null : Number(productInput.price),
|
||||
price_kg: productInput.price_kg === "" || !productInput.price_kg ? null : Number(productInput.price_kg),
|
||||
quantity: productInput.quantity === "" || !productInput.quantity ? null : Number(productInput.quantity),
|
||||
quantity_unit: productInput.quantity_unit,
|
||||
type: productInput.type!,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user