import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import Layout from '../components/Layout';
import SortTh from '../components/SortTh';
import { supabase } from '../lib/supabase';
import { useAuth } from '../context/AuthContext';
import { useSortable } from '../hooks/useSortable';
function smoothCurve(pts) {
if (pts.length < 2) return '';
let d = `M${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`;
for (let i = 1; i < pts.length; i++) {
const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i];
const cpX = (x0 + x1) / 2;
d += ` C${cpX.toFixed(1)},${y0.toFixed(1)} ${cpX.toFixed(1)},${y1.toFixed(1)} ${x1.toFixed(1)},${y1.toFixed(1)}`;
}
return d;
}
function MiniAreaChart({ data, color = '#F5A523', gradId = 'ag1' }) {
const W = 90, H = 42;
if (!data || data.length < 2) return null;
const max = Math.max(...data, 1);
const pts = data.map((v, i) => [(i / (data.length - 1)) * W, 4 + (1 - v / max) * (H - 8)]);
const line = smoothCurve(pts);
const area = `${line} L${pts[pts.length-1][0].toFixed(1)},${H} L${pts[0][0].toFixed(1)},${H} Z`;
return (
);
}
function formatTaskStatus(status) {
if (status === 'in_progress') return 'In Progress';
if (status === 'on_hold') return 'On Hold';
if (status === 'client_review') return 'In Review';
return status ? status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) : '—';
}
export default function ProfilePage() {
const { currentUser } = useAuth();
const navigate = useNavigate();
const { id: profileId } = useParams();
const [loadingProfile, setLoadingProfile] = useState(false);
const [profileError, setProfileError] = useState('');
const [viewedProfile, setViewedProfile] = useState(null);
const [viewedCompanies, setViewedCompanies] = useState([]);
const [primaryCompanyAddress, setPrimaryCompanyAddress] = useState('');
const [editOpen, setEditOpen] = useState(false);
const [savingProfile, setSavingProfile] = useState(false);
const [editError, setEditError] = useState('');
const [editForm, setEditForm] = useState({
name: '',
email: '',
website: '',
linkedin: '',
});
const [avatarUploading, setAvatarUploading] = useState(false);
const [activityItems, setActivityItems] = useState([]);
const [profileStats, setProfileStats] = useState(null);
const [profileTaskItems, setProfileTaskItems] = useState([]);
const isSelfView = !profileId || profileId === currentUser?.id;
useEffect(() => {
let cancelled = false;
async function loadViewedProfile() {
if (!profileId || profileId === currentUser?.id) {
setViewedProfile(null);
setViewedCompanies([]);
setPrimaryCompanyAddress('');
setProfileError('');
return;
}
setLoadingProfile(true);
setProfileError('');
try {
const [{ data: profile, error }] = await Promise.all([
supabase.from('profiles').select('*').eq('id', profileId).single(),
]);
if (error || !profile) {
setProfileError('Unable to load profile.');
return;
}
const [{ data: primaryCompany }, { data: memberships }] = await Promise.all([
profile.company_id
? supabase.from('companies').select('id, name, address').eq('id', profile.company_id).maybeSingle()
: Promise.resolve({ data: null }),
supabase
.from('company_members')
.select('company_id, company:companies(id, name, address)')
.eq('profile_id', profileId),
]);
if (cancelled) return;
const names = new Set();
if (primaryCompany?.name) names.add(primaryCompany.name);
const primaryAddress = primaryCompany?.address || '';
(memberships || []).forEach((row) => {
const n = row?.company?.name;
if (n) names.add(n);
});
setViewedProfile(profile);
setViewedCompanies([...names]);
setPrimaryCompanyAddress(primaryAddress);
} catch (err) {
if (!cancelled) setProfileError('Unable to load profile.');
} finally {
if (!cancelled) setLoadingProfile(false);
}
}
loadViewedProfile();
return () => {
cancelled = true;
};
}, [profileId, currentUser?.id]);
useEffect(() => {
let cancelled = false;
const uid = isSelfView ? currentUser?.id : profileId;
if (!uid) return;
const doneStatuses = ['client_approved', 'invoiced', 'paid'];
const now = new Date();
const weeksBack = 10;
const monthsBack = 8;
const weekStart = new Date(now); weekStart.setDate(weekStart.getDate() - weeksBack * 7);
const monthStart = new Date(now); monthStart.setMonth(monthStart.getMonth() - monthsBack);
Promise.all([
supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('assigned_to', uid).in('status', doneStatuses),
supabase.from('tasks').select('completed_at').eq('assigned_to', uid).in('status', doneStatuses).not('completed_at', 'is', null).gte('completed_at', weekStart.toISOString()),
supabase.from('project_members').select('project_id').eq('profile_id', uid),
supabase.from('tasks').select('project_id, submitted_at').eq('assigned_to', uid).not('project_id', 'is', null).gte('submitted_at', monthStart.toISOString()),
]).then(([completedTotal, completedRecent, members, tasksByMonth]) => {
if (cancelled) return;
// weekly chart: count completed tasks per week (oldest → newest)
const weekBuckets = Array(weeksBack).fill(0);
(completedRecent.data || []).forEach(t => {
const msAgo = now - new Date(t.completed_at);
const weekIdx = weeksBack - 1 - Math.floor(msAgo / (7 * 86400000));
if (weekIdx >= 0 && weekIdx < weeksBack) weekBuckets[weekIdx]++;
});
// monthly chart: distinct active project_ids per month
const monthBuckets = Array(monthsBack).fill(0);
const monthSets = Array.from({ length: monthsBack }, () => new Set());
(tasksByMonth.data || []).forEach(t => {
const msAgo = now - new Date(t.submitted_at);
const mIdx = monthsBack - 1 - Math.floor(msAgo / (30.44 * 86400000));
if (mIdx >= 0 && mIdx < monthsBack) monthSets[mIdx].add(t.project_id);
});
monthSets.forEach((s, i) => { monthBuckets[i] = s.size; });
const projectIds = (members.data || []).map(m => m.project_id);
const fetchActive = projectIds.length > 0
? supabase.from('projects').select('id', { count: 'exact', head: true }).in('id', projectIds).not('status', 'in', '("completed","cancelled")')
: Promise.resolve({ count: 0 });
fetchActive.then(active => {
if (cancelled) return;
setProfileStats({
tasksCompleted: completedTotal.count ?? 0,
tasksChart: weekBuckets,
activeProjects: active.count ?? 0,
projectsChart: monthBuckets,
});
});
});
return () => { cancelled = true; };
}, [isSelfView, currentUser?.id, profileId]);
useEffect(() => {
let cancelled = false;
const uid = isSelfView ? currentUser?.id : profileId;
if (!uid) return;
async function loadProfileTasks() {
const { data } = await supabase
.from('tasks')
.select('id, title, status, submitted_at')
.eq('assigned_to', uid)
.in('status', ['in_progress', 'on_hold', 'client_review'])
.order('submitted_at', { ascending: false })
.limit(20);
if (!cancelled) setProfileTaskItems(data || []);
}
loadProfileTasks();
return () => { cancelled = true; };
}, [isSelfView, currentUser?.id, profileId]);
useEffect(() => {
let cancelled = false;
const uid = isSelfView ? currentUser?.id : profileId;
if (!uid) return;
async function loadActivity() {
const [{ data: actorLogs }, { data: assignedTasks }] = await Promise.all([
supabase
.from('activity_log')
.select('id, created_at, action, task_id, task_title, project_name, project_id')
.eq('actor_id', uid)
.order('created_at', { ascending: false })
.limit(20),
supabase
.from('tasks')
.select('id')
.eq('assigned_to', uid),
]);
const assignedTaskIds = (assignedTasks || []).map((t) => t.id).filter(Boolean);
let outcomeLogs = [];
if (assignedTaskIds.length > 0) {
const { data } = await supabase
.from('activity_log')
.select('id, created_at, action, task_id, task_title, project_name, project_id')
.in('action', ['task_approved', 'revision_requested'])
.in('task_id', assignedTaskIds)
.order('created_at', { ascending: false })
.limit(20);
outcomeLogs = data || [];
}
if (cancelled) return;
const merged = [...(actorLogs || []), ...outcomeLogs];
const deduped = Array.from(new Map(merged.map((item) => [item.id, item])).values());
deduped.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
setActivityItems(deduped.slice(0, 10));
}
loadActivity();
return () => { cancelled = true; };
}, [isSelfView, currentUser?.id, profileId]);
const profile = useMemo(
() => isSelfView ? { ...(currentUser || {}), ...(viewedProfile || {}) } : viewedProfile,
// eslint-disable-next-line react-hooks/exhaustive-deps
[isSelfView, currentUser?.id, currentUser?.name, currentUser?.email, currentUser?.role, currentUser?.website, currentUser?.linkedin, currentUser?.avatar_url, viewedProfile]
);
const companyNames = useMemo(() => {
if (isSelfView) {
const names = new Set((currentUser?.companies || []).map((c) => c?.company?.name).filter(Boolean));
if (currentUser?.company?.name) names.add(currentUser.company.name);
return [...names];
}
return viewedCompanies;
}, [isSelfView, currentUser, viewedCompanies]);
const companyAddress = useMemo(() => {
if (isSelfView) return currentUser?.company?.address || currentUser?.companies?.[0]?.company?.address || '';
return primaryCompanyAddress || '';
}, [isSelfView, currentUser, primaryCompanyAddress]);
const companyDisplayName = useMemo(() => {
const role = profile?.role;
if (role === 'team' || role === 'external') return 'Fourge Branding';
if (companyNames.length > 0) return companyNames.join(', ');
return 'No Company';
}, [companyNames, profile?.role]);
const memberSince = useMemo(() => {
const d = profile?.created_at ? new Date(profile.created_at) : null;
return d && !Number.isNaN(d.getTime())
? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: null;
}, [profile?.created_at]);
const handleAvatarUpload = async (e) => {
const file = e.target.files?.[0];
if (!file || !currentUser?.id) return;
setAvatarUploading(true);
try {
const ext = file.name.split('.').pop();
const path = `${currentUser.id}/avatar.${ext}`;
const { error: upErr } = await supabase.storage.from('avatars').upload(path, file, { upsert: true });
if (upErr) { setEditError(upErr.message); return; }
const { data: { publicUrl } } = supabase.storage.from('avatars').getPublicUrl(path);
const { data, error: dbErr } = await supabase.from('profiles').update({ avatar_url: publicUrl }).eq('id', currentUser.id).select('*').single();
if (dbErr) { setEditError(dbErr.message); return; }
setViewedProfile(data);
} finally {
setAvatarUploading(false);
}
};
const dashCardStyle = {
background: 'var(--card-bg)',
border: '1px solid var(--border)',
borderRadius: 8,
padding: '18px 21px',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
};
const initials = (profile?.name || '')
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
useEffect(() => {
if (!profile) return;
setEditForm({
name: profile.name || '',
email: profile.email || '',
website: profile.website || '',
linkedin: profile.linkedin || '',
});
setEditError('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [profile?.name, profile?.email, profile?.website, profile?.linkedin]);
const setEditField = (field) => (e) => setEditForm((prev) => ({ ...prev, [field]: e.target.value }));
const handleProfileSave = async (e) => {
e.preventDefault();
if (!currentUser?.id) return;
setSavingProfile(true);
setEditError('');
try {
const payload = {
name: editForm.name.trim(),
email: editForm.email.trim(),
website: editForm.website.trim(),
linkedin: editForm.linkedin.trim(),
};
const { data, error } = await supabase
.from('profiles')
.update(payload)
.eq('id', currentUser.id)
.select('*')
.single();
if (error) {
setEditError(error.message || 'Unable to update profile.');
return;
}
setViewedProfile(data || null);
setEditOpen(false);
} finally {
setSavingProfile(false);
}
};
const ACTION_ICON = {
task_started: { bg: 'rgba(96,165,250,0.15)', color: '#60a5fa', path:
| {formatTaskStatus(task.status)} |