From 775b9200f5ce0325cb9f1752819263a0c5b034e1 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Thu, 14 May 2026 21:10:54 +0200 Subject: [PATCH] feat: group order items by product with category totals --- backend/gestion/db/db_command_items.go | 5 +- .../src/screens/admin/OrderDetailScreen.tsx | 279 +++++++++++++----- frontend-prep/src/pages/User/OrderDetails.tsx | 4 +- 3 files changed, 213 insertions(+), 75 deletions(-) diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index 259eda3e..b40004dc 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -211,6 +211,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { LivreurAssign *string `gorm:"column:livreur_assign"` CommandCreatedAt *time.Time `gorm:"column:command_created_at"` Category string `gorm:"column:category"` + Unit string `gorm:"column:unit"` ClientOrderNumber int `gorm:"column:client_order_number"` } @@ -236,7 +237,8 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { c.referral_used, c.livreur_assign, c.created_at as command_created_at, - p.category, + COALESCE(p.category, '') as category, + COALESCE(p.unit, '') as unit, c.client_order_id as client_order_number FROM command_items ci LEFT JOIN commandes c ON ci.command_id = c.id @@ -283,6 +285,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { "livreur_assign": ptrStr(row.LivreurAssign), "command_created_at": commandCreatedAt, "category": row.Category, + "unit": row.Unit, "client_order_number": row.ClientOrderNumber, } items = append(items, item) diff --git a/frontend-admin/src/screens/admin/OrderDetailScreen.tsx b/frontend-admin/src/screens/admin/OrderDetailScreen.tsx index e4aff189..92d7b672 100644 --- a/frontend-admin/src/screens/admin/OrderDetailScreen.tsx +++ b/frontend-admin/src/screens/admin/OrderDetailScreen.tsx @@ -409,6 +409,81 @@ export default function OrderDetailScreen() { marginTop: spacing.xs, }, + // Grouped items + subItemRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: spacing.xs, + paddingLeft: spacing.m, + borderLeftWidth: 2, + borderLeftColor: colors.border, + marginLeft: spacing.xs, + marginBottom: 2, + }, + subItemText: { + color: colors.textSecondary, + fontSize: fontSize.sm, + flex: 1, + }, + groupTotalRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginTop: spacing.s, + paddingTop: spacing.s, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + groupTotalQty: { + color: colors.textWhite, + fontSize: fontSize.sm, + fontWeight: "700", + }, + groupTotalPrice: { + color: colors.accent, + fontSize: fontSize.md, + fontWeight: "700", + }, + categoryBadge: { + fontSize: fontSize.xs, + color: colors.accent, + fontWeight: "600", + marginRight: spacing.s, + textTransform: "uppercase", + }, + // Category summary + categorySummaryCard: { + marginTop: spacing.s, + }, + categoryRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: spacing.s, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + categoryName: { + color: colors.textSecondary, + fontSize: fontSize.sm, + fontWeight: "600", + flex: 1, + }, + categoryQty: { + color: colors.textWhite, + fontSize: fontSize.sm, + fontWeight: "700", + marginRight: spacing.l, + }, + categoryTotal: { + color: colors.accent, + fontSize: fontSize.sm, + fontWeight: "700", + minWidth: 70, + textAlign: "right", + }, + // Items modal itemsModalOverlay: { flex: 1, @@ -476,6 +551,25 @@ export default function OrderDetailScreen() { [colors, MAP_HEIGHT], ); + // Groupement des items par product_id + const productGroups = items.reduce>((acc, item) => { + const key = String(item.product_id || item.produit); + if (!acc[key]) acc[key] = []; + acc[key].push(item); + return acc; + }, {}); + const groupedList = Object.values(productGroups); + + // Récapitulatif par catégorie + const categoryTotals = items.reduce>((acc, item) => { + const cat = item.category || "Autre"; + if (!acc[cat]) acc[cat] = { qty: 0, total: 0 }; + acc[cat].qty += item.quantite ?? 0; + acc[cat].total += item.prix ?? 0; + return acc; + }, {}); + const categoryEntries = Object.entries(categoryTotals); + if (loading) return ; if (!command) return ( @@ -742,40 +836,66 @@ export default function OrderDetailScreen() { )} - {/* Items */} + {/* Items groupés par produit */} Articles ({items.length}) - {items.map((item: any) => ( - - - - {item.produit ?? item.product_name} - - - handleDeleteItem( - item.id, - item.produit ?? item.product_name, - ) - } - > - - - - - - Quantité: {item.quantite} - - - Prix: {item.prix?.toFixed(2)} € - - - - ))} + {groupedList.map((group, gi) => { + const rep = group[0]; + const name = rep.produit ?? rep.product_name; + const unit = rep.unit || ""; + const totalQty = group.reduce((s: number, it: any) => s + (it.quantite ?? 0), 0); + const totalPrice = group.reduce((s: number, it: any) => s + (it.prix ?? 0), 0); + const isMultiple = group.length > 1; + return ( + + + {name} + {rep.category ? ( + {rep.category} + ) : null} + handleDeleteItem(rep.id, name)} + > + + + + {isMultiple && group.map((item: any, i: number) => ( + + + {item.quantite}{unit} — {item.prix?.toFixed(2)} € + + handleDeleteItem(item.id, name)} + > + + + + ))} + + + {isMultiple ? `Total: ${totalQty}${unit}` : `${totalQty}${unit}`} + + {totalPrice.toFixed(2)} € + + + ); + })} + + {/* Récapitulatif par catégorie */} + {categoryEntries.length > 0 && ( + <> + Par catégorie + + {categoryEntries.map(([cat, data]) => ( + + {cat} + {data.qty.toFixed(data.qty % 1 === 0 ? 0 : 2)} + {data.total.toFixed(2)} € + + ))} + + + )} {/* Actions */} Actions @@ -865,48 +985,61 @@ export default function OrderDetailScreen() { - {items.map((item: any) => ( - - - - {item.produit ?? item.product_name} - - { - setShowItemsModal(false); - handleDeleteItem( - item.id, - item.produit ?? - item.product_name, - ); - }} - > - - - - - - Quantité: {item.quantite} - - - Prix: {item.prix?.toFixed(2)} € - + {groupedList.map((group, gi) => { + const rep = group[0]; + const name = rep.produit ?? rep.product_name; + const unit = rep.unit || ""; + const totalQty = group.reduce((s: number, it: any) => s + (it.quantite ?? 0), 0); + const totalPrice = group.reduce((s: number, it: any) => s + (it.prix ?? 0), 0); + const isMultiple = group.length > 1; + return ( + + + {name} + {rep.category ? ( + {rep.category} + ) : null} + { + setShowItemsModal(false); + handleDeleteItem(rep.id, name); + }} + > + + + + {isMultiple && group.map((item: any) => ( + + + {item.quantite}{unit} — {item.prix?.toFixed(2)} € + + + ))} + + + {isMultiple ? `Total: ${totalQty}${unit}` : `${totalQty}${unit}`} + + {totalPrice.toFixed(2)} € + + ); + })} + {/* Récapitulatif catégories dans le modal */} + {categoryEntries.length > 0 && ( + + + Par catégorie + + {categoryEntries.map(([cat, data]) => ( + + {cat} + {data.qty.toFixed(data.qty % 1 === 0 ? 0 : 2)} + {data.total.toFixed(2)} € + + ))} - ))} + )} {items.length === 0 && ( Aucun article )} diff --git a/frontend-prep/src/pages/User/OrderDetails.tsx b/frontend-prep/src/pages/User/OrderDetails.tsx index ca780c7f..e0f00f42 100644 --- a/frontend-prep/src/pages/User/OrderDetails.tsx +++ b/frontend-prep/src/pages/User/OrderDetails.tsx @@ -60,7 +60,9 @@ function OrderDetails() { const location = useLocation(); // commandId = ID global pour l'API (passé en state depuis l'historique) // fallback sur orderId si navigation directe via URL - const commandId = (location.state as { commandId?: number } | null)?.commandId ?? parseInt(orderId ?? "0"); + const commandId = + (location.state as { commandId?: number } | null)?.commandId ?? + parseInt(orderId ?? "0"); const [order, setOrder] = useState(null); const [enrichedProducts, setEnrichedProducts] = useState<