canina/frontend/application/lib/store/userStore.ts

293 lines
9.5 KiB
TypeScript

"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { authService } from "../services/authService";
import api from "../services/api";
import { useCartStore } from "./cartStore";
import { usePetStore } from "./usePetStore";
export type UserRole = "User_Guest" | "User_PetOwner" | "User_Partner";
export interface Address {
id: string;
title: string;
receptorName: string;
phone: string;
province: string;
city: string;
detail: string;
zipCode: string;
isDefault: boolean;
}
export interface Transaction {
id: string;
type: 'top_up' | 'purchase';
amount: number;
date: string;
status: 'success' | 'failed' | 'pending';
}
interface UserProfile {
firstName: string;
lastName: string;
email: string;
mobile: string;
walletBalance: number;
charityDonationTotal: number;
addresses: Address[];
transactions: Transaction[];
}
interface UserStore {
role: UserRole;
isLoggedIn: boolean;
isAuthModalOpen: boolean;
profile: UserProfile;
setRole: (role: UserRole) => void;
setLoggedIn: (isLoggedIn: boolean) => void;
setAuthModalOpen: (open: boolean) => void;
updateProfile: (profile: Partial<UserProfile>) => Promise<void>;
addAddress: (address: Address) => Promise<void>;
updateAddress: (id: string, address: Address) => Promise<void>;
deleteAddress: (id: string) => Promise<void>;
setDefaultAddress: (id: string) => Promise<void>;
topUpWallet: (amount: number) => Promise<void>;
logout: () => void;
fetchProfile: () => Promise<void>;
}
export const useUserStore = create<UserStore>()(
persist(
(set) => ({
role: "User_Guest",
isLoggedIn: false,
isAuthModalOpen: false,
profile: {
firstName: "",
lastName: "",
email: "",
mobile: "",
walletBalance: 0,
charityDonationTotal: 0,
addresses: [],
transactions: [],
},
setRole: (role) => set({ role }),
setLoggedIn: (isLoggedIn) => set({ isLoggedIn }),
setAuthModalOpen: (open) => set({ isAuthModalOpen: open }),
updateProfile: async (updates) => {
try {
const profileData = await authService.updateProfile(updates);
set((state) => ({
profile: {
...state.profile,
firstName: profileData.firstName,
lastName: profileData.lastName,
email: profileData.email,
mobile: profileData.mobile || "",
}
}));
} catch (error) {
console.error("Failed to update profile:", error);
throw error;
}
},
addAddress: async (address) => {
try {
// Remove ID so backend generates UUID
const addressData = {
title: address.title,
receptorName: address.receptorName,
phone: address.phone,
province: address.province,
city: address.city,
detail: address.detail,
zipCode: address.zipCode,
isDefault: address.isDefault,
};
await api.post('/users/addresses', addressData);
await useUserStore.getState().fetchProfile();
} catch (error) {
console.error("Failed to add address:", error);
throw error;
}
},
updateAddress: async (id, updated) => {
try {
const addressData = {
title: updated.title,
receptorName: updated.receptorName,
phone: updated.phone,
province: updated.province,
city: updated.city,
detail: updated.detail,
zipCode: updated.zipCode,
isDefault: updated.isDefault,
};
await api.patch(`/users/addresses/${id}`, addressData);
await useUserStore.getState().fetchProfile();
} catch (error) {
console.error("Failed to update address:", error);
throw error;
}
},
deleteAddress: async (id) => {
try {
await api.delete(`/users/addresses/${id}`);
await useUserStore.getState().fetchProfile();
} catch (error) {
console.error("Failed to delete address:", error);
throw error;
}
},
setDefaultAddress: async (id) => {
try {
await api.patch(`/users/addresses/${id}/default`);
await useUserStore.getState().fetchProfile();
} catch (error) {
console.error("Failed to set default address:", error);
throw error;
}
},
topUpWallet: async (amount) => {
try {
await api.post('/users/wallet/top-up', { amount });
await useUserStore.getState().fetchProfile();
} catch (error) {
console.error("Failed to top-up wallet:", error);
throw error;
}
},
logout: () => {
authService.logout();
try { usePetStore.getState().reset(); } catch {}
try { localStorage.removeItem("canina-pets"); } catch {}
set({
role: "User_Guest",
isLoggedIn: false,
profile: {
firstName: "",
lastName: "",
email: "",
mobile: "",
walletBalance: 0,
charityDonationTotal: 0,
addresses: [],
transactions: []
}
});
},
fetchProfile: async () => {
try {
const token = typeof window !== 'undefined' && window.localStorage ? localStorage.getItem('accessToken') : (globalThis as any)._testToken;
if (!token) {
try { usePetStore.getState().reset(); } catch {}
try { localStorage.removeItem("canina-pets"); } catch {}
set({
isLoggedIn: false,
role: "User_Guest",
profile: {
firstName: "",
lastName: "",
email: "",
mobile: "",
walletBalance: 0,
charityDonationTotal: 0,
addresses: [],
transactions: []
}
});
return;
}
const profileData = await authService.getProfile();
if (!profileData) {
authService.logout();
set({
isLoggedIn: false,
role: "User_Guest",
profile: {
firstName: "",
lastName: "",
email: "",
mobile: "",
walletBalance: 0,
charityDonationTotal: 0,
addresses: [],
transactions: []
}
});
return;
}
set((state) => {
const pd = profileData as unknown as Record<string, unknown>;
const backendWallet = Number(profileData.walletBalance || 0);
const backendCharity = Number(profileData.charityDonationTotal || 0);
const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string }>)?.map((t) => ({
id: t.id,
type: (t.type === 'deposit' ? 'top_up' : 'purchase') as Transaction['type'],
amount: Number(t.amount),
date: t.createdAt,
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status']
})) || [];
return {
isLoggedIn: true,
role: (profileData.role as UserRole) || "User_PetOwner",
profile: {
firstName: profileData.firstName,
lastName: profileData.lastName,
email: profileData.email,
mobile: profileData.mobile || "",
walletBalance: backendWallet,
charityDonationTotal: backendCharity,
addresses: Array.isArray(pd.addresses) && pd.addresses.length > 0 ? (pd.addresses as Address[]) : state.profile.addresses,
transactions: backendTransactions.length > 0 ? backendTransactions : state.profile.transactions
}
};
});
// Sync orders list to CartStore while preserving local petId & address mappings
if (profileData.orders) {
const currentLocalOrders = useCartStore.getState().orders;
const mergedOrders = (profileData.orders as Record<string, unknown>[]).map((bo) => {
const localMatch = currentLocalOrders.find(lo => lo.id === bo.id);
return {
...bo,
petId: bo.petId || (bo.pet as { id?: string })?.id || localMatch?.petId,
shippingAddress: bo.shippingAddress || localMatch?.shippingAddress
};
});
useCartStore.getState().setOrders(mergedOrders);
}
// Sync pets list to PetStore
if (profileData.pets) {
usePetStore.getState().setPets(profileData.pets as Record<string, unknown>[]);
}
} catch (error) {
console.error("Failed to fetch user profile:", error);
authService.logout();
set({
isLoggedIn: false,
role: "User_Guest",
profile: {
firstName: "",
lastName: "",
email: "",
mobile: "",
walletBalance: 0,
charityDonationTotal: 0,
addresses: [],
transactions: []
}
});
}
},
}),
{ name: "canina-user" }
)
);