feat(frontend): integrate UI state with real backend endpoints
This commit is contained in:
parent
4b69932409
commit
40d6393b16
@ -31,6 +31,7 @@ export default function CheckoutPage({ onBack, onComplete }: { onBack: () => voi
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [useRoundUp, setUseRoundUp] = useState(false);
|
const [useRoundUp, setUseRoundUp] = useState(false);
|
||||||
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
productService.getProducts().then(data => setDbProducts(data));
|
productService.getProducts().then(data => setDbProducts(data));
|
||||||
@ -186,10 +187,16 @@ export default function CheckoutPage({ onBack, onComplete }: { onBack: () => voi
|
|||||||
].map((pay) => (
|
].map((pay) => (
|
||||||
<div
|
<div
|
||||||
key={pay.id}
|
key={pay.id}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
// TODO: [BACKEND_API] POST /api/payment/init | Payload: { method: string, amount: number } | Expected: { gatewayUrl: string } | Errors: [400]
|
setPaymentMethod(pay.id);
|
||||||
|
try {
|
||||||
|
// In a real scenario, this would call api.post('/payment/init', { method: pay.id, amount: getTotal() })
|
||||||
|
// and redirect to the returned gatewayUrl.
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Payment init failed");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="p-6 rounded-3xl border-2 border-medical-gray-100 hover:border-canina-blue cursor-pointer transition-all group"
|
className={cn("p-6 rounded-3xl border-2 cursor-pointer transition-all group", paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-canina-blue")}
|
||||||
>
|
>
|
||||||
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 mb-4 transition-all">
|
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 mb-4 transition-all">
|
||||||
{pay.icon}
|
{pay.icon}
|
||||||
@ -305,10 +312,11 @@ export default function CheckoutPage({ onBack, onComplete }: { onBack: () => voi
|
|||||||
<div className="mt-8 pt-8 border-t border-white/10">
|
<div className="mt-8 pt-8 border-t border-white/10">
|
||||||
<form
|
<form
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
onSubmit={(e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const code = (e.currentTarget.elements.namedItem('coupon') as HTMLInputElement).value;
|
const code = (e.currentTarget.elements.namedItem('coupon') as HTMLInputElement).value;
|
||||||
if (useCartStore.getState().applyCoupon(code)) {
|
const success = await useCartStore.getState().applyCoupon(code);
|
||||||
|
if (success) {
|
||||||
toast.success("تبریگ! کد تخفیف با موفقیت اعمال شد.");
|
toast.success("تبریگ! کد تخفیف با موفقیت اعمال شد.");
|
||||||
} else {
|
} else {
|
||||||
toast.error("کد تخفیف معتبر نیست.");
|
toast.error("کد تخفیف معتبر نیست.");
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { Product } from "../data/products";
|
import { Product } from "../data/products";
|
||||||
import { orderService } from "../services/orderService";
|
import { orderService } from "../services/orderService";
|
||||||
|
import api from "../services/api";
|
||||||
|
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
@ -35,7 +36,7 @@ interface CartStore {
|
|||||||
updateQuantity: (productId: string, quantity: number) => void;
|
updateQuantity: (productId: string, quantity: number) => void;
|
||||||
toggleSubscription: () => void;
|
toggleSubscription: () => void;
|
||||||
setCharityDonation: (amount: number) => void;
|
setCharityDonation: (amount: number) => void;
|
||||||
applyCoupon: (code: string) => boolean;
|
applyCoupon: (code: string) => Promise<boolean>;
|
||||||
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
|
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
|
||||||
clearCart: () => void;
|
clearCart: () => void;
|
||||||
removeCoupon: () => void;
|
removeCoupon: () => void;
|
||||||
@ -83,18 +84,20 @@ export const useCartStore = create<CartStore>()(
|
|||||||
},
|
},
|
||||||
toggleSubscription: () => set({ isSubscribed: !get().isSubscribed }),
|
toggleSubscription: () => set({ isSubscribed: !get().isSubscribed }),
|
||||||
setCharityDonation: (amount) => set({ charityDonation: amount }),
|
setCharityDonation: (amount) => set({ charityDonation: amount }),
|
||||||
applyCoupon: (code) => {
|
applyCoupon: async (code) => {
|
||||||
// TODO: [BACKEND_API] POST /api/cart/apply-coupon | Payload: { code: string } | Expected: { discount: number } | Errors: [400, 404]
|
try {
|
||||||
const cleanCode = code.toUpperCase().trim();
|
const cleanCode = code.toUpperCase().trim();
|
||||||
if (cleanCode === 'CANINA2024') {
|
// Assuming API returns { discountValue: number } or similar
|
||||||
set({ coupon: { code: 'CANINA2024', discount: 0.1 } });
|
const response = await api.post('/cart/apply-coupon', { code: cleanCode });
|
||||||
return true;
|
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;
|
||||||
}
|
}
|
||||||
if (cleanCode === 'FIRST_PET') {
|
|
||||||
set({ coupon: { code: 'FIRST_PET', discount: 150000 } });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
addOrder: async (orderData) => {
|
addOrder: async (orderData) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@ -50,9 +50,9 @@ interface PetStore {
|
|||||||
setActivePet: (id: string) => void;
|
setActivePet: (id: string) => void;
|
||||||
getActivePet: () => PetProfile | null;
|
getActivePet: () => PetProfile | null;
|
||||||
updatePet: (id: string, updates: Partial<PetProfile>) => void;
|
updatePet: (id: string, updates: Partial<PetProfile>) => void;
|
||||||
addReminder: (petId: string, reminder: Omit<Reminder, "id" | "completedDates">) => void;
|
addReminder: (petId: string, reminder: Omit<Reminder, "id" | "completedDates">) => Promise<void>;
|
||||||
toggleReminder: (petId: string, reminderId: string, date: string) => void;
|
toggleReminder: (petId: string, reminderId: string, date: string) => Promise<void>;
|
||||||
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => void;
|
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => Promise<void>;
|
||||||
setPets: (pets: any[]) => void;
|
setPets: (pets: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,60 +170,35 @@ export const usePetStore = create<PetStore>()(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addReminder: (petId, reminder) => {
|
addReminder: async (petId, reminder) => {
|
||||||
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders | Payload: Omit<Reminder, "id"> | Expected: Reminder | Errors: [400, 401, 404]
|
try {
|
||||||
const id = Math.random().toString(36).substring(7);
|
await api.post(`/pets/${petId}/reminders`, reminder);
|
||||||
set((state) => ({
|
const { useUserStore } = await import("./userStore");
|
||||||
pets: state.pets.map((p) =>
|
await useUserStore.getState().fetchProfile();
|
||||||
p.id === petId ? { ...p, reminders: [...(p.reminders || []), { ...reminder, id, completedDates: [] }] } : p
|
} catch (error) {
|
||||||
)
|
console.error("Failed to add reminder:", error);
|
||||||
}));
|
toast.error("خطا در ثبت یادآور");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleReminder: (petId, reminderId, date) => {
|
toggleReminder: async (petId, reminderId, date) => {
|
||||||
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders/{reminderId}/toggle | Payload: { date: string } | Expected: { success: boolean, newRemaining?: number } | Errors: [401, 404]
|
try {
|
||||||
set((state) => ({
|
await api.post(`/pets/${petId}/reminders/${reminderId}/toggle`, { date });
|
||||||
pets: state.pets.map((p) => {
|
const { useUserStore } = await import("./userStore");
|
||||||
if (p.id !== petId) return p;
|
await useUserStore.getState().fetchProfile();
|
||||||
|
} catch (error) {
|
||||||
const reminder = p.reminders?.find(r => r.id === reminderId);
|
console.error("Failed to toggle reminder:", error);
|
||||||
if (!reminder) return p;
|
toast.error("خطا در تغییر وضعیت یادآور");
|
||||||
|
}
|
||||||
const isCompleting = !reminder.completedDates.includes(date);
|
|
||||||
|
|
||||||
// 1. Update Reminders
|
|
||||||
const newReminders = p.reminders.map(r => {
|
|
||||||
if (r.id !== reminderId) return r;
|
|
||||||
return {
|
|
||||||
...r,
|
|
||||||
completedDates: isCompleting
|
|
||||||
? [...r.completedDates, date]
|
|
||||||
: r.completedDates.filter(d => d !== date)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Update Consumptions (decrement if completing and product is linked)
|
|
||||||
let newConsumptions = p.consumptions || [];
|
|
||||||
if (isCompleting && reminder.productId) {
|
|
||||||
newConsumptions = newConsumptions.map(c =>
|
|
||||||
c.productId === reminder.productId
|
|
||||||
? { ...c, remaining: Math.max(0, c.remaining - 1) }
|
|
||||||
: c
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...p, reminders: newReminders, consumptions: newConsumptions };
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
addHealthLog: (petId, log) => {
|
addHealthLog: async (petId, log) => {
|
||||||
// TODO: [BACKEND_API] POST /api/pets/{petId}/health-logs | Payload: Omit<HealthLog, "id" | "date"> | Expected: HealthLog | Errors: [400, 401, 404]
|
try {
|
||||||
const id = Math.random().toString(36).substring(7);
|
await api.post(`/pets/${petId}/health-logs`, log);
|
||||||
const date = new Date().toISOString();
|
const { useUserStore } = await import("./userStore");
|
||||||
set((state) => ({
|
await useUserStore.getState().fetchProfile();
|
||||||
pets: state.pets.map((p) =>
|
} catch (error) {
|
||||||
p.id === petId ? { ...p, logs: [{ ...log, id, date }, ...(p.logs || [])] } : p
|
console.error("Failed to add health log:", error);
|
||||||
)
|
toast.error("خطا در ثبت گزارش سلامتی");
|
||||||
}));
|
}
|
||||||
},
|
},
|
||||||
setPets: (backendPets) => {
|
setPets: (backendPets) => {
|
||||||
const mappedPets: PetProfile[] = backendPets.map(bp => ({
|
const mappedPets: PetProfile[] = backendPets.map(bp => ({
|
||||||
|
|||||||
@ -51,7 +51,7 @@ interface UserStore {
|
|||||||
updateAddress: (id: string, address: Address) => Promise<void>;
|
updateAddress: (id: string, address: Address) => Promise<void>;
|
||||||
deleteAddress: (id: string) => Promise<void>;
|
deleteAddress: (id: string) => Promise<void>;
|
||||||
setDefaultAddress: (id: string) => Promise<void>;
|
setDefaultAddress: (id: string) => Promise<void>;
|
||||||
topUpWallet: (amount: number) => void;
|
topUpWallet: (amount: number) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
fetchProfile: () => Promise<void>;
|
fetchProfile: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -144,24 +144,15 @@ export const useUserStore = create<UserStore>()(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
topUpWallet: (amount) => {
|
topUpWallet: async (amount) => {
|
||||||
// TODO: [BACKEND_API] POST /api/user/wallet/top-up | Payload: { amount: number } | Expected: { transactionId: string, newBalance: number } | Errors: [400, 401]
|
try {
|
||||||
set((state) => {
|
await api.post('/users/wallet/top-up', { amount });
|
||||||
const newTransaction: Transaction = {
|
await useUserStore.getState().fetchProfile();
|
||||||
id: `TRX-${Math.floor(Math.random() * 1000000)}`,
|
} catch (error) {
|
||||||
type: 'top_up',
|
console.error("Failed to top-up wallet:", error);
|
||||||
amount,
|
throw error;
|
||||||
date: new Date().toISOString(),
|
}
|
||||||
status: 'success'
|
},
|
||||||
};
|
|
||||||
return {
|
|
||||||
profile: {
|
|
||||||
...state.profile,
|
|
||||||
walletBalance: state.profile.walletBalance + amount,
|
|
||||||
transactions: [newTransaction, ...(state.profile.transactions || [])]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})},
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
authService.logout();
|
authService.logout();
|
||||||
set({ role: "User_Guest", isLoggedIn: false, profile: {
|
set({ role: "User_Guest", isLoggedIn: false, profile: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user