chore: build

This commit is contained in:
2026-06-16 21:17:54 +02:00
parent 440792a066
commit ec7e59550b
11 changed files with 868 additions and 1 deletions
+43
View File
@@ -2201,6 +2201,49 @@ export const getMyPointsRewards = async (): Promise<{
}
};
export const submitLivreurRating = async (
orderId: number,
rating: number,
comment: string,
): Promise<{ success: boolean; error?: string }> => {
const token = getAuthToken();
if (!token) return { success: false, error: "Non authentifié" };
try {
const response = await fetch(`${API_URL}/orders/${orderId}/rate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ rating, comment }),
});
if (!response.ok) {
const data = await safeJson(response);
return { success: false, error: data.error || "Erreur" };
}
return { success: true };
} catch {
return { success: false, error: "Erreur de connexion" };
}
};
export const getOrderRatingStatus = async (
orderId: number,
): Promise<{ rated: boolean; rating?: number; comment?: string }> => {
const token = getAuthToken();
if (!token) return { rated: false };
try {
const response = await fetch(`${API_URL}/orders/${orderId}/rating`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return { rated: false };
const data = await safeJson(response);
return data;
} catch {
return { rated: false };
}
};
export const claimMyReward = async (poolKey: string): Promise<{
success: boolean;
description?: string;
@@ -802,6 +802,148 @@
animation-delay: 0.4s;
}
/* ============================================
RATING LIVREUR
============================================ */
.rating-btn {
display: flex;
align-items: center;
gap: 0.4rem;
background: #f59e0b;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
margin-top: 0.75rem;
transition: opacity 0.2s;
}
.rating-btn:hover {
opacity: 0.85;
}
.rating-done-row {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
background: rgba(245, 158, 11, 0.1);
border-radius: 6px;
}
.rating-done-text {
color: #f59e0b;
font-size: 0.85rem;
}
.rating-star-filled {
color: #f59e0b;
}
.rating-star-empty {
color: #6b7280;
}
.rating-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.rating-modal-sheet {
background: var(--surface, #1a1a2e);
border-radius: 16px 16px 0 0;
padding: 1.5rem 1.5rem 2.5rem;
width: 100%;
max-width: 500px;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rating-modal-title {
color: var(--text-primary, #fff);
font-size: 1.1rem;
font-weight: 700;
margin: 0;
}
.rating-modal-subtitle {
color: var(--text-muted, #9ca3af);
font-size: 0.875rem;
margin: 0;
}
.rating-stars-row {
display: flex;
gap: 0.5rem;
margin: 0.5rem 0;
}
.rating-star-btn {
background: none;
border: none;
padding: 0;
cursor: pointer;
line-height: 0;
}
.rating-comment-input {
width: 100%;
background: var(--bg, #0f0f1a);
border: 1px solid var(--border, #374151);
border-radius: 6px;
color: var(--text-primary, #fff);
padding: 0.75rem;
font-size: 0.875rem;
resize: vertical;
box-sizing: border-box;
}
.rating-comment-input::placeholder {
color: var(--text-muted, #9ca3af);
}
.rating-submit-btn {
background: #f59e0b;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.75rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.rating-submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.rating-submit-btn:not(:disabled):hover {
opacity: 0.85;
}
.rating-cancel-btn {
background: none;
border: none;
color: var(--text-muted, #9ca3af);
font-size: 0.875rem;
cursor: pointer;
padding: 0.5rem;
text-align: center;
}
/* ============================================
PRINT STYLES
============================================ */
@@ -9,6 +9,8 @@ import {
isUserAuthenticated,
getProductById,
getMediaUrl,
submitLivreurRating,
getOrderRatingStatus,
} from "../../api/api";
import type { CompletedOrder, Product } from "../../api/api_types";
import {
@@ -26,6 +28,7 @@ import {
ShoppingBag,
Camera,
X,
Star,
} from "lucide-react";
interface OrderProduct {
@@ -73,6 +76,11 @@ function OrderDetails() {
const [error, setError] = useState<string>("");
const [showVideo, setShowVideo] = useState(false);
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
const [ratingModal, setRatingModal] = useState(false);
const [selectedStars, setSelectedStars] = useState(0);
const [ratingComment, setRatingComment] = useState("");
const [ratingSubmitting, setRatingSubmitting] = useState(false);
const [ratingDone, setRatingDone] = useState<{ rating: number; comment: string } | null>(null);
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
@@ -315,6 +323,11 @@ function OrderDetails() {
console.log("✅ [ORDER DETAILS] Données mappées:", orderData);
setOrder(orderData);
if (apiData.command_info?.command_status === "approved" && apiData.command_info?.livreur_assign) {
const r = await getOrderRatingStatus(id);
if (r.rated) setRatingDone({ rating: r.rating!, comment: r.comment ?? "" });
}
} else {
console.error("❌ [ORDER DETAILS] Erreur:", result.message);
setError(result.message || "Erreur lors du chargement");
@@ -327,6 +340,17 @@ function OrderDetails() {
}
};
const handleSubmitRating = async () => {
if (selectedStars === 0 || !order) return;
setRatingSubmitting(true);
const res = await submitLivreurRating(order.id, selectedStars, ratingComment);
setRatingSubmitting(false);
if (res.success) {
setRatingDone({ rating: selectedStars, comment: ratingComment });
setRatingModal(false);
}
};
const getProductCount = (totalPrix: number): number => {
return Math.max(1, Math.round(totalPrix / 25));
};
@@ -446,6 +470,22 @@ function OrderDetails() {
</div>
)}
{order.status === "approved" && order.livreur_assign && (
ratingDone ? (
<div className="rating-done-row">
<Star size={14} className="rating-star-filled" />
<span className="rating-done-text">
Noté {ratingDone.rating}/5{ratingDone.comment ? ` · "${ratingDone.comment}"` : ""}
</span>
</div>
) : (
<button className="rating-btn" onClick={() => setRatingModal(true)}>
<Star size={14} />
Noter le livreur
</button>
)
)}
<div className="info-row">
<div className="info-icon">
<Calendar size={18} />
@@ -759,6 +799,45 @@ function OrderDetails() {
</div>
</div>
{/* Modal notation livreur */}
{ratingModal && (
<div className="rating-modal-overlay" onClick={() => setRatingModal(false)}>
<div className="rating-modal-sheet" onClick={(e) => e.stopPropagation()}>
<h2 className="rating-modal-title">Noter le livreur</h2>
<p className="rating-modal-subtitle">{order.livreur_assign}</p>
<div className="rating-stars-row">
{[1, 2, 3, 4, 5].map((star) => (
<button key={star} className="rating-star-btn" onClick={() => setSelectedStars(star)}>
<Star
size={36}
className={star <= selectedStars ? "rating-star-filled" : "rating-star-empty"}
fill={star <= selectedStars ? "#f59e0b" : "none"}
/>
</button>
))}
</div>
<textarea
className="rating-comment-input"
placeholder="Commentaire (optionnel)"
value={ratingComment}
onChange={(e) => setRatingComment(e.target.value)}
maxLength={500}
rows={3}
/>
<button
className="rating-submit-btn"
onClick={handleSubmitRating}
disabled={selectedStars === 0 || ratingSubmitting}
>
{ratingSubmitting ? "Envoi..." : "Envoyer ma note"}
</button>
<button className="rating-cancel-btn" onClick={() => setRatingModal(false)}>
Annuler
</button>
</div>
</div>
)}
{/* Modal vidéo */}
{showVideo && currentVideoUrl && (
<div className="video-modal" onClick={handleCloseVideo}>