chore: build
This commit is contained in:
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user