307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
"use client";
|
|
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
import { authService, User } 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' | 'refund';
|
|
amount: number;
|
|
date: string;
|
|
status: 'success' | 'failed' | 'pending';
|
|
description?: string;
|
|
transactionReference?: string;
|
|
}
|
|
|
|
interface UserProfile {
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
mobile: string;
|
|
walletBalance: number;
|
|
charityDonationTotal: number;
|
|
hasPassword?: boolean;
|
|
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 & { password?: string; currentPassword?: string }>) => 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,
|
|
hasPassword: false,
|
|
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 as Partial<User>);
|
|
set((state) => ({
|
|
profile: {
|
|
...state.profile,
|
|
firstName: profileData.firstName,
|
|
lastName: profileData.lastName,
|
|
email: profileData.email,
|
|
mobile: profileData.mobile || "",
|
|
hasPassword: profileData.hasPassword !== undefined ? profileData.hasPassword : state.profile.hasPassword,
|
|
}
|
|
}));
|
|
} 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 {
|
|
let token: string | null = null;
|
|
try {
|
|
token = localStorage.getItem('accessToken');
|
|
} catch {}
|
|
if (!token) {
|
|
token = (globalThis as unknown as { _testToken?: string })._testToken || null;
|
|
}
|
|
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; description?: string; transactionReference?: string }>)?.map((t) => ({
|
|
id: t.id,
|
|
type: (t.type === 'deposit' ? 'top_up' : t.type === 'refund' ? 'refund' : 'purchase') as Transaction['type'],
|
|
amount: Number(t.amount),
|
|
date: t.createdAt,
|
|
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status'],
|
|
description: t.description || undefined,
|
|
transactionReference: t.transactionReference || undefined
|
|
})) || [];
|
|
|
|
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,
|
|
hasPassword: profileData.hasPassword !== undefined ? profileData.hasPassword : state.profile.hasPassword,
|
|
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" }
|
|
)
|
|
);
|