469 lines
19 KiB
TypeScript
469 lines
19 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import useAuthStore from '../store/authStore'
|
|
import Header from '../components/nav/Header'
|
|
import Footer from '../components/Footer'
|
|
import ProfileCompletion from './components/profileCompletion'
|
|
import BasicInformation from './components/basicInformation'
|
|
import MediaSection from './components/mediaSection'
|
|
import BankInformation from './components/bankInformation'
|
|
import EditModal from './components/editModal'
|
|
import {
|
|
UserCircleIcon,
|
|
EnvelopeIcon,
|
|
PhoneIcon,
|
|
MapPinIcon,
|
|
PencilIcon,
|
|
CheckCircleIcon
|
|
} from '@heroicons/react/24/outline'
|
|
import { getProfileCompletion } from './hooks/getProfileCompletion';
|
|
import { useProfileData } from './hooks/getProfileData';
|
|
import { useMedia } from './hooks/getMedia';
|
|
import { editProfileBasic, editProfileBank } from './hooks/editProfile';
|
|
|
|
// Helper to display missing fields in subtle gray italic (no yellow highlight)
|
|
function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return (
|
|
<span className="italic text-gray-400">
|
|
Not provided
|
|
</span>
|
|
);
|
|
}
|
|
return <>{children}</>;
|
|
}
|
|
|
|
// Helper for safe access to profileData fields
|
|
function getProfileField<T extends keyof typeof defaultProfileData>(
|
|
obj: typeof defaultProfileData,
|
|
key: T
|
|
) {
|
|
return obj[key];
|
|
}
|
|
|
|
// Default profile data for typing
|
|
const defaultProfileData = {
|
|
firstName: '',
|
|
lastName: '',
|
|
email: '',
|
|
phone: '',
|
|
address: '',
|
|
joinDate: '',
|
|
memberStatus: '',
|
|
profileComplete: 0,
|
|
accountHolder: '',
|
|
iban: '',
|
|
contactPersonName: '',
|
|
userType: '',
|
|
};
|
|
|
|
export default function ProfilePage() {
|
|
const router = useRouter()
|
|
const user = useAuthStore(state => state.user)
|
|
const [userId, setUserId] = React.useState<string | number | undefined>(undefined);
|
|
|
|
// Update userId when user changes
|
|
useEffect(() => {
|
|
if (user?.id) setUserId(user.id);
|
|
}, [user]);
|
|
|
|
// Add refresh key and UI states for smooth refresh
|
|
const [refreshKey, setRefreshKey] = React.useState(0);
|
|
const [showRefreshing, setShowRefreshing] = React.useState(false);
|
|
const [completionLoading, setCompletionLoading] = React.useState(false);
|
|
|
|
// Fetch profile data on page load/navigation, now with refreshKey
|
|
const { data: profileDataApi, loading: profileLoading, error: profileError } = useProfileData(userId, refreshKey);
|
|
|
|
// Fetch media/documents for user, now with refreshKey
|
|
const { data: mediaData, loading: mediaLoading, error: mediaError } = useMedia(userId, refreshKey);
|
|
|
|
// Redirect if not logged in
|
|
useEffect(() => {
|
|
if (!user) {
|
|
router.push('/login')
|
|
}
|
|
}, [user, router])
|
|
|
|
// Don't render if no user
|
|
if (!user) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
|
|
<p className="text-[#4A4A4A]">Loading...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Progress bar state
|
|
const [progressPercent, setProgressPercent] = React.useState<number>(0);
|
|
const [completedSteps, setCompletedSteps] = React.useState<string[]>([]);
|
|
const [allSteps, setAllSteps] = React.useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!user) {
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
async function fetchCompletion() {
|
|
setCompletionLoading(true);
|
|
const progress = await getProfileCompletion();
|
|
// progress can be percent or object
|
|
if (progress && typeof progress === 'object') {
|
|
// If not admin-verified, cap progress below 100 to reflect pending verification
|
|
const pct = progress.progressPercent ?? 0;
|
|
// Try to read admin verification from profileDataApi if available; otherwise assume false until data loads
|
|
const isAdminVerified = Boolean(profileDataApi?.userStatus?.is_admin_verified);
|
|
setProgressPercent(isAdminVerified ? pct : Math.min(pct, 95));
|
|
setCompletedSteps(progress.completedSteps ?? []);
|
|
setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? []);
|
|
} else if (typeof progress === 'number') {
|
|
setProgressPercent(progress);
|
|
}
|
|
setCompletionLoading(false);
|
|
}
|
|
fetchCompletion();
|
|
}, [user, router, refreshKey]);
|
|
|
|
// If admin verification flips to true, ensure progress shows 100%
|
|
useEffect(() => {
|
|
const verified = Boolean(profileDataApi?.userStatus?.is_admin_verified);
|
|
if (verified) {
|
|
setProgressPercent(prev => (prev < 100 ? 100 : prev));
|
|
}
|
|
}, [profileDataApi?.userStatus?.is_admin_verified]);
|
|
|
|
// Use API profile data if available, fallback to mock
|
|
const profileData = React.useMemo(() => {
|
|
if (!profileDataApi) {
|
|
return {
|
|
firstName: 'Admin',
|
|
lastName: 'User',
|
|
email: user?.email || 'office@profit-planet.com',
|
|
phone: '+49 123 456 789',
|
|
address: 'Musterstraße 123, 12345 Berlin',
|
|
joinDate: 'Oktober 2024',
|
|
memberStatus: 'Gold Member',
|
|
profileComplete: progressPercent,
|
|
accountHolder: '', // Always empty string if not provided
|
|
iban: '',
|
|
contactPersonName: '',
|
|
userType: user?.userType || '',
|
|
};
|
|
}
|
|
const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi;
|
|
return {
|
|
firstName: apiUser.firstName ?? apiProfile.first_name ?? '',
|
|
lastName: apiUser.lastName ?? apiProfile.last_name ?? '',
|
|
email: apiUser.email ?? '',
|
|
phone: apiUser.phone ?? apiProfile.phone ?? '',
|
|
address: apiProfile.address ?? '',
|
|
joinDate: apiUser.createdAt
|
|
? new Date(apiUser.createdAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'long' })
|
|
: '',
|
|
memberStatus: userStatus.status ?? '',
|
|
profileComplete: progressPercent,
|
|
accountHolder: apiProfile.account_holder_name ?? '', // Only use account_holder_name
|
|
iban: apiUser.iban ?? '',
|
|
contactPersonName: apiProfile.contact_person_name ?? '',
|
|
userType: apiUser.userType ?? '',
|
|
};
|
|
}, [profileDataApi, user, progressPercent]);
|
|
|
|
// Dummy data for new sections
|
|
const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : [];
|
|
|
|
// Adjusted bankInfo state to only have accountHolder and iban, always strings
|
|
const [bankInfo, setBankInfo] = React.useState({
|
|
accountHolder: '',
|
|
iban: '',
|
|
});
|
|
const [editingBank, setEditingBank] = React.useState(false);
|
|
const [bankDraft, setBankDraft] = React.useState(bankInfo)
|
|
|
|
// Modal state
|
|
const [editModalOpen, setEditModalOpen] = React.useState(false);
|
|
const [editModalType, setEditModalType] = React.useState<'basic' | 'bank'>('basic');
|
|
const [editModalValues, setEditModalValues] = React.useState<Record<string, string>>({});
|
|
|
|
// Modal error state
|
|
const [editModalError, setEditModalError] = React.useState<string | null>(null);
|
|
|
|
// Modal field definitions
|
|
const basicFields = [
|
|
{ key: 'firstName', label: 'First Name' },
|
|
{ key: 'lastName', label: 'Last Name' },
|
|
{ key: 'email', label: 'Email Address', type: 'email' },
|
|
{ key: 'phone', label: 'Phone Number' },
|
|
{ key: 'address', label: 'Address' },
|
|
];
|
|
const bankFields = [
|
|
{ key: 'accountHolder', label: 'Account Holder' },
|
|
{ key: 'iban', label: 'IBAN' },
|
|
];
|
|
|
|
// Modal open handlers
|
|
function openEditModal(type: 'basic' | 'bank', values: Record<string, string>) {
|
|
setEditModalType(type);
|
|
setEditModalValues(values);
|
|
setEditModalOpen(true);
|
|
}
|
|
|
|
// Modal save handler (calls API)
|
|
async function handleEditModalSave() {
|
|
setEditModalError(null);
|
|
if (editModalType === 'basic') {
|
|
const payload: Partial<typeof defaultProfileData> = {};
|
|
(['firstName', 'lastName', 'email', 'phone', 'address'] as const).forEach(key => {
|
|
if (editModalValues[key] !== getProfileField(profileData, key)) {
|
|
payload[key] = editModalValues[key]?.trim();
|
|
}
|
|
});
|
|
const res = await editProfileBasic(payload);
|
|
if (res.success) {
|
|
setEditModalOpen(false);
|
|
// Start smooth refresh with overlay spinner
|
|
setShowRefreshing(true);
|
|
setRefreshKey(k => k + 1);
|
|
} else if (res.status === 409) {
|
|
setEditModalError('Email already in use.');
|
|
} else if (res.status === 401) {
|
|
router.push('/login');
|
|
} else {
|
|
setEditModalError(res.error || 'Failed to update profile.');
|
|
}
|
|
} else {
|
|
const payload: Partial<typeof defaultProfileData> = {};
|
|
(['accountHolder', 'iban'] as const).forEach(key => {
|
|
if (editModalValues[key] !== getProfileField(profileData, key)) {
|
|
payload[key] = editModalValues[key]?.trim();
|
|
}
|
|
});
|
|
const res = await editProfileBank(payload);
|
|
if (res.success) {
|
|
setBankInfo({
|
|
accountHolder: res.data?.profile?.account_holder_name ?? '',
|
|
iban: res.data?.user?.iban ?? '',
|
|
});
|
|
setEditModalOpen(false);
|
|
// Start smooth refresh with overlay spinner
|
|
setShowRefreshing(true);
|
|
setRefreshKey(k => k + 1);
|
|
} else if (res.status === 400 && res.error?.toLowerCase().includes('iban')) {
|
|
setEditModalError('Invalid IBAN.');
|
|
} else if (res.status === 401) {
|
|
router.push('/login');
|
|
} else {
|
|
setEditModalError(res.error || 'Failed to update bank info.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Modal change handler
|
|
function handleEditModalChange(key: string, value: string) {
|
|
setEditModalValues(prev => ({ ...prev, [key]: value }));
|
|
}
|
|
|
|
// Hide overlay when all data re-fetches complete
|
|
useEffect(() => {
|
|
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
|
|
const t = setTimeout(() => setShowRefreshing(false), 200); // small delay for smoothness
|
|
return () => clearTimeout(t);
|
|
}
|
|
}, [showRefreshing, profileLoading, mediaLoading, completionLoading]);
|
|
|
|
const loadingUser = !user;
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col bg-gray-50" suppressHydrationWarning>
|
|
<Header />
|
|
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-4xl mx-auto">
|
|
{loadingUser && (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
|
|
<p className="text-[#4A4A4A]">Loading...</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{!loadingUser && (
|
|
<>
|
|
{/* Page Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
|
|
<p className="text-gray-600 mt-2">
|
|
Manage your account information and preferences
|
|
</p>
|
|
</div>
|
|
|
|
{/* Pending admin verification notice (above progress) */}
|
|
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
|
|
<div className="rounded-md bg-yellow-50 border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
|
|
Your account is fully submitted. Our team will verify your account shortly.
|
|
</div>
|
|
)}
|
|
{/* Profile Completion Progress Bar */}
|
|
<ProfileCompletion
|
|
profileComplete={profileData.profileComplete}
|
|
/>
|
|
|
|
{/* Basic Info + Sidebar */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
|
{/* Basic Information */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<BasicInformation
|
|
profileData={profileData}
|
|
HighlightIfMissing={HighlightIfMissing}
|
|
// Add edit button handler
|
|
onEdit={() => openEditModal('basic', {
|
|
firstName: profileData.firstName,
|
|
lastName: profileData.lastName,
|
|
email: profileData.email,
|
|
phone: profileData.phone,
|
|
address: profileData.address,
|
|
})}
|
|
/>
|
|
</div>
|
|
{/* Sidebar: Account Status + Quick Actions */}
|
|
<div className="space-y-6">
|
|
{/* Account Status */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
|
|
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-600">Member Since</span>
|
|
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-600">Status</span>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-[#8D6B1D] to-[#C49225] text-white">
|
|
{profileData.memberStatus}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-600">Profile</span>
|
|
<span className="text-sm font-medium text-green-600">Verified</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Quick Actions */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
|
|
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={() => router.push('/dashboard')}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
|
>
|
|
Go to Dashboard
|
|
</button>
|
|
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors">
|
|
Download Account Data
|
|
</button>
|
|
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
|
Delete Account
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bank Info, Media */}
|
|
<div className="space-y-8 mb-8">
|
|
{/* --- Edit Bank Information Section --- */}
|
|
<BankInformation
|
|
profileData={profileData}
|
|
editingBank={editingBank}
|
|
bankDraft={bankDraft}
|
|
setEditingBank={setEditingBank}
|
|
setBankDraft={setBankDraft}
|
|
setBankInfo={setBankInfo}
|
|
// Add edit button handler
|
|
onEdit={() => openEditModal('bank', {
|
|
accountHolder: profileData.accountHolder,
|
|
iban: profileData.iban,
|
|
})}
|
|
/>
|
|
{/* --- Media Section --- */}
|
|
<MediaSection documents={documents} />
|
|
</div>
|
|
|
|
{/* Account Settings */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-6">Account Settings</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between py-3 border-b border-gray-100">
|
|
<div>
|
|
<p className="font-medium text-gray-900">Email Notifications</p>
|
|
<p className="text-sm text-gray-600">Receive updates about orders and promotions</p>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" className="sr-only peer" defaultChecked />
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#8D6B1D]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#8D6B1D]"></div>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-3 border-b border-gray-100">
|
|
<div>
|
|
<p className="font-medium text-gray-900">SMS Notifications</p>
|
|
<p className="text-sm text-gray-600">Get text messages for important updates</p>
|
|
</div>
|
|
<label className="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" className="sr-only peer" />
|
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#8D6B1D]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#8D6B1D]"></div>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between py-3">
|
|
<div>
|
|
<p className="font-medium text-gray-900">Two-Factor Authentication</p>
|
|
<p className="text-sm text-gray-600">Add extra security to your account</p>
|
|
</div>
|
|
<button className="px-4 py-2 text-sm font-medium text-[#8D6B1D] border border-[#8D6B1D] rounded-lg hover:bg-[#8D6B1D]/10 transition-colors">
|
|
Enable
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</main>
|
|
<Footer />
|
|
|
|
{/* Global refreshing overlay */}
|
|
{showRefreshing && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/60 backdrop-blur-sm">
|
|
<div className="flex flex-col items-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-[#8D6B1D]/30 border-t-[#8D6B1D] mb-3"></div>
|
|
<p className="text-sm text-gray-700">Updating...</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit Modal */}
|
|
<EditModal
|
|
open={editModalOpen}
|
|
type={editModalType}
|
|
fields={editModalType === 'basic' ? basicFields : bankFields}
|
|
values={editModalValues}
|
|
onChange={handleEditModalChange}
|
|
onSave={handleEditModalSave}
|
|
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
|
|
>
|
|
{/* Show error message if present */}
|
|
{editModalError && (
|
|
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
|
|
)}
|
|
</EditModal>
|
|
</div>
|
|
)
|
|
} |