chore: build
Backend - Build & Lint / build (push) Failing after 30m2s
Frontend Client - EAS Build / build (push) Canceled after 7m55s
Frontend Web - Build & Lint / build (push) Failing after 10m18s

This commit is contained in:
Xor290
2026-08-21 13:08:56 +02:00
parent b9073954f1
commit 46652bb925
23 changed files with 549 additions and 404 deletions
+2 -50
View File
@@ -1440,55 +1440,6 @@ export const cancelCommand = async (
}
};
// Le client corrige lui-même l'adresse de sa commande (refusé si déjà en
// livraison ou terminée, cf. UpdateOwnCommandAddress côté backend).
export const updateOwnCommandAddress = async (
commandId: number,
deliveryAddress: string,
): Promise<{ success: boolean; message: string }> => {
const token = sessionStorage.getItem("token");
if (!token) {
return { success: false, message: "Session invalide" };
}
try {
const response = await fetch(
`${API_URL}/commands/${commandId}/address`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ delivery_address: deliveryAddress }),
},
);
const data = await safeJson(response);
if (!response.ok) {
return {
success: false,
message: data.error || "Erreur lors de la mise à jour",
};
}
return {
success: true,
message: data.message || "Adresse mise à jour",
};
} catch (error) {
return {
success: false,
message:
error instanceof Error
? error.message
: "Erreur lors de la mise à jour de l'adresse",
};
}
};
/**
* ✅ GET MY CANCELLATION HISTORY - Historique des annulations
* GET /api/v1/my-cancellation-history
@@ -2217,6 +2168,7 @@ export const unlinkTelegram = async (): Promise<void> => {
export type RewardCategoryConfig = {
category: string;
type: "free_product" | "half_price_product";
all_products: boolean;
product_ids: number[];
product_names: string[];
@@ -2228,6 +2180,7 @@ export type RewardItemConfig = {
product_name: string;
quantity: number;
price: number;
type: "free_product" | "half_price_product";
};
export type PointsPoolInfo = {
@@ -2243,7 +2196,6 @@ export type PointsPoolInfo = {
export type PointsRewardConfig = {
threshold: number;
type: string;
description: string;
reward_items: RewardItemConfig[];
};
+8 -3
View File
@@ -5,6 +5,8 @@ import { useNavigate } from "react-router-dom";
import { isUserAuthenticated, getProductById, getMediaUrl } from "../../api/api";
import type { Product } from "../../api/api";
import { Trash2, ShoppingCart, AlertTriangle, Leaf, X } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGift } from "@fortawesome/free-solid-svg-icons";
import "./Cart.css";
interface CartItemWithMedia {
@@ -180,15 +182,18 @@ function Cart() {
<p className="cart-row-name">
{item.name_product}
{item.is_reward && (
<span style={{ marginLeft: "6px", fontSize: "0.7rem", fontWeight: 700, color: "#f59e0b", background: "rgba(245,158,11,0.12)", borderRadius: "4px", padding: "1px 6px" }}>
🎁 Récompense
<span style={{ display: "inline-flex", alignItems: "center", gap: "4px", marginLeft: "6px", fontSize: "0.7rem", fontWeight: 700, color: "#f59e0b", background: "rgba(245,158,11,0.12)", borderRadius: "4px", padding: "1px 6px" }}>
<FontAwesomeIcon icon={faGift} />
Récompense
</span>
)}
</p>
<p className="cart-row-qty">{item.quantity}g</p>
<p className="cart-row-price">
{item.is_reward ? (
{item.is_reward && item.price === 0 ? (
<span style={{ color: "#10b981", fontWeight: 700 }}>Offert</span>
) : item.is_reward ? (
<span style={{ color: "#10b981", fontWeight: 700 }}>{item.price.toFixed(2)} </span>
) : (
`${item.price.toFixed(2)}`
)}
@@ -210,7 +210,7 @@ function ConsultationHistorique() {
if (res.success) {
const text =
res.product_added && res.product_names?.length
? `🎁 ${res.product_names.join(", ")} ajouté${res.product_names.length > 1 ? "s" : ""} à votre panier ! Commandez au moins un produit pour en profiter.`
? `${res.product_names.join(", ")} ajouté${res.product_names.length > 1 ? "s" : ""} à votre panier ! Commandez au moins un produit pour en profiter.`
: res.description || "Récompense réclamée !";
setClaimFeedback({ pool: poolKey, type: "success", text });
getMyPointsRewards().then((r) => {
@@ -505,6 +505,15 @@ function ConsultationHistorique() {
<p
className={`reward-feedback reward-feedback-${feedback.type}`}
>
{feedback.type ===
"success" && (
<FontAwesomeIcon
icon={faGift}
style={{
marginRight: 6,
}}
/>
)}
{feedback.text}
</p>
)}
@@ -747,13 +756,18 @@ function ConsultationHistorique() {
item.quantity !== 1
? `×${item.quantity}`
: ""}
{item.price > 0
? ` · valeur ${item.price.toFixed(2)}`
{item.type ===
"half_price_product" &&
item.price > 0
? ` · ${item.price.toFixed(2)}`
: ""}
</span>
</div>
<span className="reward-product-free">
Offert
{item.type ===
"half_price_product"
? "-50%"
: "Offert"}
</span>
</button>
);
@@ -594,4 +594,55 @@
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Modal stock insuffisant */
.stock-warning-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.stock-warning-modal-content {
position: relative;
width: 100%;
max-width: 340px;
padding: 2rem;
text-align: center;
background: #1a1a1a;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
color: #fff;
}
.stock-warning-close-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: transparent;
border: none;
color: #aaa;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.stock-warning-ok-btn {
background: #ef4444;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.6rem 1.5rem;
font-weight: 700;
cursor: pointer;
}
@@ -1,5 +1,7 @@
import { useParams, useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react";
import {
getProductById,
getCategories,
@@ -25,6 +27,7 @@ function ProductDetail() {
// floats
const [selectedGrams, setSelectedGrams] = useState<number | null>(null);
const [selectedPrice, setSelectedPrice] = useState<number>(0.0);
const [stockWarning, setStockWarning] = useState<{ wanted: number; available: number } | null>(null);
// ✅ TOAST STATE
const [toast, setToast] = useState<{
@@ -171,6 +174,11 @@ function ProductDetail() {
return;
}
if (product.stock > 0 && selectedGrams > product.stock) {
setStockWarning({ wanted: selectedGrams, available: product.stock });
return;
}
addToCart({
product_id: product.id,
name_product: product.name,
@@ -363,6 +371,50 @@ function ProductDetail() {
</div>
</div>
</div>
{/* Modal stock insuffisant */}
{stockWarning &&
createPortal(
<div
className="stock-warning-modal"
onClick={() => setStockWarning(null)}
>
<div
className="stock-warning-modal-content"
onClick={(e) => e.stopPropagation()}
>
<button
className="stock-warning-close-btn"
onClick={() => setStockWarning(null)}
aria-label="Fermer"
>
<X size={18} />
</button>
<div style={{ fontSize: "2.5rem", marginBottom: "0.75rem" }}>
</div>
<p style={{ fontWeight: 700, fontSize: "1.1rem", marginBottom: "0.5rem" }}>
Stock insuffisant
</p>
<p style={{ color: "#aaa", fontSize: "0.95rem", marginBottom: "1.25rem" }}>
Vous avez sélectionné{" "}
<strong>{stockWarning.wanted}{product.unit || "g"}</strong> mais il ne
reste que{" "}
<strong style={{ color: "#ef4444" }}>
{stockWarning.available}{product.unit || "g"}
</strong>{" "}
disponible pour <em>{product.name}</em>.
</p>
<button
className="stock-warning-ok-btn"
onClick={() => setStockWarning(null)}
>
OK
</button>
</div>
</div>,
document.body,
)}
</>
);
}
@@ -489,40 +489,6 @@
font-weight: 600;
color: var(--text);
}
.address-edit-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.4rem;
background: none;
border: none;
color: var(--primary);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
padding: 0;
}
.address-edit-toggle:hover {
text-decoration: underline;
}
.address-edit {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.address-edit-input {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 0.85rem;
background: var(--bg-secondary, #fff);
color: var(--text);
}
.address-edit-actions {
display: flex;
gap: 0.5rem;
}
.contact {
color: var(--text-muted);
font-size: 0.85rem;
@@ -14,7 +14,6 @@ import {
getOrderETA,
confirmReception,
cancelCommand,
updateOwnCommandAddress,
isUserAuthenticated,
getPublicSettings,
} from "../../api/api";
@@ -52,7 +51,6 @@ import {
faWind,
faChevronUp,
faChevronDown,
faPen,
} from "@fortawesome/free-solid-svg-icons";
interface OrderWithTracking extends OrderDetail {
@@ -284,11 +282,6 @@ function SuiviLivraison() {
const [showCancelDialog, setShowCancelDialog] = useState(false);
const [orderToCancel, setOrderToCancel] = useState<number | null>(null);
const [cancelReason, setCancelReason] = useState("");
const [editingAddressOrder, setEditingAddressOrder] = useState<
number | null
>(null);
const [newAddressValue, setNewAddressValue] = useState("");
const [savingAddress, setSavingAddress] = useState(false);
const [showPenaltyWarning, setShowPenaltyWarning] = useState(false);
const [penaltyWarningData, setPenaltyWarningData] =
useState<CancelCommandResponse | null>(null);
@@ -390,28 +383,6 @@ function SuiviLivraison() {
}
};
const handleUpdateAddress = async (orderId: number) => {
setSavingAddress(true);
try {
const res = await updateOwnCommandAddress(
orderId,
newAddressValue,
);
if (res.success) {
showToast(res.message || "Adresse mise à jour", "success");
setEditingAddressOrder(null);
setNewAddressValue("");
loadOrders();
} else {
showToast(res.message || "Erreur", "error");
}
} catch {
showToast("Erreur mise à jour adresse", "error");
} finally {
setSavingAddress(false);
}
};
const showToast = (
message: string,
type: "success" | "error" | "warning" | "info",
@@ -918,88 +889,6 @@ function SuiviLivraison() {
order,
)}
</p>
{(statusLow ===
"pending" ||
statusLow ===
"assigned") &&
(editingAddressOrder ===
order.id ? (
<div className="address-edit">
<input
type="text"
className="address-edit-input"
value={
newAddressValue
}
onChange={(
e,
) =>
setNewAddressValue(
e
.target
.value,
)
}
placeholder="Ex: 24 Rue Docteur Brindeau, 44000 Nantes"
/>
<div className="address-edit-actions">
<button
type="button"
className="btn-secondary"
onClick={() => {
setEditingAddressOrder(
null,
);
setNewAddressValue(
"",
);
}}
>
Annuler
</button>
<button
type="button"
className="btn-primary"
disabled={
savingAddress ||
!newAddressValue.trim()
}
onClick={() =>
handleUpdateAddress(
order.id,
)
}
>
{savingAddress
? "Enregistrement..."
: "Enregistrer"}
</button>
</div>
</div>
) : (
<button
type="button"
className="address-edit-toggle"
onClick={() => {
setNewAddressValue(
getDeliveryAddress(
order,
),
);
setEditingAddressOrder(
order.id,
);
}}
>
<FontAwesomeIcon
icon={
faPen
}
/>{" "}
Modifier
l'adresse
</button>
))}
{(() => {
const info =
getClientInfo(