profit-planet-frontend/src/app/profile/page.tsx

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>
)
}