- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
"use client";
|
|
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
import { Product } from "../data/products";
|
|
import { orderService } from "../services/orderService";
|
|
import api from "../services/api";
|
|
|
|
|
|
export interface CartItem {
|
|
product: Product;
|
|
quantity: number;
|
|
calculatedDose?: {
|
|
quantity: number;
|
|
unit: string;
|
|
};
|
|
}
|
|
|
|
interface Order {
|
|
id: string;
|
|
date: string;
|
|
items: CartItem[];
|
|
total: number;
|
|
charityDonation?: number;
|
|
status: 'processing' | 'shipped' | 'delivered';
|
|
petId?: string;
|
|
trackingNumber: string;
|
|
}
|
|
|
|
interface CartStore {
|
|
items: CartItem[];
|
|
isSubscribed: boolean;
|
|
charityDonation: number;
|
|
coupon: { code: string; discount: number } | null;
|
|
orders: Order[];
|
|
addItem: (product: Product, quantity: number, dose?: { quantity: number; unit: string }) => void;
|
|
removeItem: (productId: string) => void;
|
|
updateQuantity: (productId: string, quantity: number) => void;
|
|
toggleSubscription: () => void;
|
|
setCharityDonation: (amount: number) => void;
|
|
applyCoupon: (code: string) => Promise<boolean>;
|
|
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
|
|
clearCart: () => void;
|
|
removeCoupon: () => void;
|
|
setOrders: (orders: any[]) => void;
|
|
getTotalItems: () => number;
|
|
getSubtotal: () => number;
|
|
getDiscount: () => number;
|
|
getTotal: () => number;
|
|
}
|
|
|
|
export const useCartStore = create<CartStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
items: [],
|
|
isSubscribed: false,
|
|
charityDonation: 0,
|
|
coupon: null,
|
|
orders: [],
|
|
addItem: (product, quantity, dose) => {
|
|
const items = get().items;
|
|
const existing = items.find((i) => i.product.id === product.id);
|
|
if (existing) {
|
|
set({
|
|
items: items.map((i) =>
|
|
i.product.id === product.id ? { ...i, quantity: i.quantity + quantity } : i
|
|
),
|
|
});
|
|
} else {
|
|
set({ items: [...items, { product, quantity, calculatedDose: dose }] });
|
|
}
|
|
},
|
|
removeItem: (productId) => {
|
|
set({ items: get().items.filter((i) => i.product.id !== productId) });
|
|
},
|
|
updateQuantity: (productId, quantity) => {
|
|
if (quantity <= 0) {
|
|
get().removeItem(productId);
|
|
return;
|
|
}
|
|
set({
|
|
items: get().items.map((i) =>
|
|
i.product.id === productId ? { ...i, quantity } : i
|
|
),
|
|
});
|
|
},
|
|
toggleSubscription: () => set({ isSubscribed: !get().isSubscribed }),
|
|
setCharityDonation: (amount) => set({ charityDonation: amount }),
|
|
applyCoupon: async (code) => {
|
|
try {
|
|
const cleanCode = code.toUpperCase().trim();
|
|
const subtotal = get().getSubtotal();
|
|
const response = await api.post('/orders/validate-coupon', { code: cleanCode, cartTotal: subtotal });
|
|
if (response.data && response.data.discountValue) {
|
|
set({ coupon: { code: cleanCode, discount: response.data.discountValue } });
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error("Failed to apply coupon:", error);
|
|
return false;
|
|
}
|
|
},
|
|
addOrder: async (orderData) => {
|
|
const payload = {
|
|
petId: orderData.petId,
|
|
items: orderData.items.map(i => ({
|
|
productId: i.product.id,
|
|
quantity: i.quantity
|
|
}))
|
|
};
|
|
|
|
try {
|
|
const backendOrder = await orderService.createOrder(payload);
|
|
|
|
const newOrder: Order = {
|
|
...orderData,
|
|
id: backendOrder.id,
|
|
date: backendOrder.createdAt,
|
|
total: Number(backendOrder.totalAmount),
|
|
charityDonation: Number(backendOrder.charityDonation),
|
|
status: backendOrder.status as any,
|
|
trackingNumber: backendOrder.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
|
|
};
|
|
|
|
set({ orders: [newOrder, ...get().orders] });
|
|
return backendOrder.id;
|
|
} catch (error: any) {
|
|
console.error("Order creation failed on backend:", error);
|
|
throw error;
|
|
}
|
|
},
|
|
clearCart: () => set({ items: [], coupon: null }),
|
|
removeCoupon: () => set({ coupon: null }),
|
|
setOrders: (backendOrders) => {
|
|
const mappedOrders: Order[] = backendOrders.map(bo => ({
|
|
id: bo.id,
|
|
date: bo.createdAt,
|
|
items: bo.orderItems?.map((oi: any) => ({
|
|
product: oi.product,
|
|
quantity: oi.quantity
|
|
})) || [],
|
|
total: Number(bo.totalAmount),
|
|
charityDonation: Number(bo.charityDonation),
|
|
status: bo.status,
|
|
trackingNumber: bo.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
|
|
}));
|
|
set({ orders: mappedOrders });
|
|
},
|
|
getTotalItems: () => get().items.reduce((acc, item) => acc + item.quantity, 0),
|
|
getSubtotal: () => get().items.reduce((acc, item) => acc + item.product.priceValue * item.quantity, 0),
|
|
getDiscount: () => {
|
|
const subtotal = get().getSubtotal();
|
|
let discount = get().isSubscribed ? subtotal * 0.05 : 0;
|
|
|
|
if (get().coupon) {
|
|
const c = get().coupon!;
|
|
if (c.discount < 1) {
|
|
discount += subtotal * c.discount;
|
|
} else {
|
|
discount += c.discount;
|
|
}
|
|
}
|
|
|
|
return Math.min(discount, subtotal);
|
|
},
|
|
getTotal: () => get().getSubtotal() - get().getDiscount() + get().charityDonation,
|
|
}),
|
|
{ name: "canina-cart" }
|
|
)
|
|
);
|