114 lines
2.9 KiB
React
114 lines
2.9 KiB
React
|
|
import {
|
|||
|
|
createContext,
|
|||
|
|
useCallback,
|
|||
|
|
useContext,
|
|||
|
|
useEffect,
|
|||
|
|
useMemo,
|
|||
|
|
useState,
|
|||
|
|
} from 'react'
|
|||
|
|
|
|||
|
|
const STORAGE_KEY = 'librairie-orders'
|
|||
|
|
|
|||
|
|
function loadOrders() {
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|||
|
|
if (raw === null) return []
|
|||
|
|
const parsed = JSON.parse(raw)
|
|||
|
|
return Array.isArray(parsed) ? parsed : []
|
|||
|
|
} catch {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveOrders(orders) {
|
|||
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(orders))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function round2(n) {
|
|||
|
|
return Math.round((Number(n) + Number.EPSILON) * 100) / 100
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function calcSubtotal(items) {
|
|||
|
|
return round2(
|
|||
|
|
items.reduce((sum, it) => sum + Number(it.unitPrice) * Number(it.qty), 0),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function calcDiscount({ subtotal, promotion }) {
|
|||
|
|
if (!promotion) return 0
|
|||
|
|
if (promotion.type === 'percent') {
|
|||
|
|
const pct = Number(promotion.value)
|
|||
|
|
if (!Number.isFinite(pct) || pct <= 0) return 0
|
|||
|
|
return round2((subtotal * pct) / 100)
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const OrdersContext = createContext(null)
|
|||
|
|
|
|||
|
|
export function OrdersProvider({ children }) {
|
|||
|
|
const [orders, setOrders] = useState(loadOrders)
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
saveOrders(orders)
|
|||
|
|
}, [orders])
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Équivalent local d’un POST /api/orders.
|
|||
|
|
* orderDraft: { items: [{ bookId, title, unitPrice, qty }], promotion?: { code, type, value } }
|
|||
|
|
*/
|
|||
|
|
const createOrder = useCallback((orderDraft) => {
|
|||
|
|
const items = Array.isArray(orderDraft?.items) ? orderDraft.items : []
|
|||
|
|
const cleaned = items
|
|||
|
|
.map((it) => ({
|
|||
|
|
bookId: String(it.bookId),
|
|||
|
|
title: String(it.title || ''),
|
|||
|
|
unitPrice: Number(it.unitPrice),
|
|||
|
|
qty: Math.max(0, Math.trunc(Number(it.qty) || 0)),
|
|||
|
|
}))
|
|||
|
|
.filter((it) => it.bookId && it.qty > 0 && Number.isFinite(it.unitPrice))
|
|||
|
|
|
|||
|
|
if (cleaned.length === 0) {
|
|||
|
|
throw new Error('Aucun article dans la commande')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const subtotal = calcSubtotal(cleaned)
|
|||
|
|
const promotion = orderDraft?.promotion
|
|||
|
|
? {
|
|||
|
|
code: String(orderDraft.promotion.code || ''),
|
|||
|
|
type: String(orderDraft.promotion.type || 'percent'),
|
|||
|
|
value: Number(orderDraft.promotion.value),
|
|||
|
|
}
|
|||
|
|
: null
|
|||
|
|
const discount = calcDiscount({ subtotal, promotion })
|
|||
|
|
const total = round2(Math.max(0, subtotal - discount))
|
|||
|
|
|
|||
|
|
const order = {
|
|||
|
|
id: crypto.randomUUID(),
|
|||
|
|
createdAt: new Date().toISOString(),
|
|||
|
|
items: cleaned,
|
|||
|
|
promotion: promotion && promotion.code ? promotion : null,
|
|||
|
|
subtotal,
|
|||
|
|
discount,
|
|||
|
|
total,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setOrders((prev) => [order, ...prev])
|
|||
|
|
return order
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const value = useMemo(() => ({ orders, createOrder }), [orders, createOrder])
|
|||
|
|
|
|||
|
|
return <OrdersContext.Provider value={value}>{children}</OrdersContext.Provider>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|||
|
|
export function useOrders() {
|
|||
|
|
const ctx = useContext(OrdersContext)
|
|||
|
|
if (!ctx) {
|
|||
|
|
throw new Error('useOrders doit être utilisé dans un OrdersProvider')
|
|||
|
|
}
|
|||
|
|
return ctx
|
|||
|
|
}
|
|||
|
|
|