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
|
||
}
|
||
|