import React, { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { X, Copy, Check, Users, Crown, LogOut, Loader2, LayoutGrid, ChevronRight, PenLine, Search, Plus, AtSign, HelpCircle, ArrowLeft, ChevronLeft, Trash2 } from 'lucide-react'; import { useAuth } from '@/contexts/SupabaseAuthContext'; import { supabase } from '@/lib/customSupabaseClient'; import UserAvatarDisplay from '@/components/UserAvatarDisplay'; import VerifiedBadge from '@/components/VerifiedBadge'; import WhiteArrowSendIcon from '@/components/WhiteArrowSendIcon'; import WorkspacePanel, { ChannelChat, amcGlassCard, amcGlassCancel, amcGlassDanger, amcGlassPrimary } from '@/components/ProjectDashboard'; import { fetchCollaborationDocument, downloadCollaborationStatement } from '@/components/KlikCommentsPanel'; import { HelpSection } from '@/components/UserSettingsPage'; import { useAssistant } from '@/contexts/AssistantContext'; // ============================================================================ // Espace de travail (Programme Partenaire) : accueil « Mes équipes » en style // conversation, puis chaque équipe en 4 onglets (Aperçu, Discussion, Membres, // Outils). Jusqu'à 3 équipes par partenaire. Règle des commissions d'équipe : // 50 % pour celui qui a amené le filleul, 50 % partagés à parts égales. // ============================================================================ const TEAM_EMOJIS = ['🚀', '🏙', '💼', '🎯', '🔥', '🌍', '👗', '🍽']; const TeamManagementModal = ({ onClose, personalReferralCode }) => { const { user } = useAuth(); const [loading, setLoading] = useState(true); const [teams, setTeams] = useState([]); const [current, setCurrent] = useState(null); // équipe ouverte const [tab, setTab] = useState('apercu'); const [members, setMembers] = useState([]); const [discussionId, setDiscussionId] = useState(null); const [showCreate, setShowCreate] = useState(false); const [newTeamName, setNewTeamName] = useState(''); const [newTeamEmoji, setNewTeamEmoji] = useState('🚀'); const [showJoin, setShowJoin] = useState(false); const [joinTeamId, setJoinTeamId] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [copied, setCopied] = useState(''); const [confirm, setConfirm] = useState(null); // { title, body, action, label } const [showWorkspace, setShowWorkspace] = useState(null); // Pilotage (étape 2) : tableau de bord, tâches, projets et budget, activité, gains du mois. const [dash, setDash] = useState(null); const [earnings, setEarnings] = useState(null); const [showEarnings, setShowEarnings] = useState(false); const [subTool, setSubTool] = useState(null); // 'taches' | 'projets' | 'activite' const [tasks, setTasks] = useState([]); const [newTask, setNewTask] = useState(''); const [newTaskAssignee, setNewTaskAssignee] = useState(''); const [projects, setProjects] = useState([]); const [newProject, setNewProject] = useState(''); const [myPurchases, setMyPurchases] = useState(null); const [attachProject, setAttachProject] = useState(''); const [activity, setActivity] = useState([]); const [goalDraft, setGoalDraft] = useState(null); const [teamCollabs, setTeamCollabs] = useState([]); // Inviter quelqu'un : recherche par nom, invitation envoyée dans ses messages. const [inviteQuery, setInviteQuery] = useState(''); const [inviteResults, setInviteResults] = useState([]); const [invitedIds, setInvitedIds] = useState(new Set()); // Suivi des invitations envoyées par l'équipe. const [invitations, setInvitations] = useState([]); const [showInvites, setShowInvites] = useState(false); const [inviteFilter, setInviteFilter] = useState('all'); const loadInvitations = async (teamId) => { const { data } = await supabase.rpc('get_team_invitations', { p_team_id: teamId }); setInvitations((data || []).sort((a, b) => new Date(b.sent_at) - new Date(a.sent_at))); }; // Écrire à l'invité : l'Espace de travail se ferme, puis la conversation // AirMessenger s'ouvre (jamais les deux en même temps). // Discussion privée avec un invité, dans un panneau à part de l'Espace de // travail. L'invité reçoit et répond dans sa messagerie habituelle. const [openDm, setOpenDm] = useState(null); const [dmMessages, setDmMessages] = useState([]); const [dmText, setDmText] = useState(''); const dmEndRef = useRef(null); const loadDm = async (otherId, scroll = true) => { const { data } = await supabase.from('messages').select('id, sender_id, recipient_id, content, created_at, read_at, type') .or(`and(sender_id.eq.${user.id},recipient_id.eq.${otherId}),and(sender_id.eq.${otherId},recipient_id.eq.${user.id})`) .order('created_at', { ascending: true }).limit(200); setDmMessages(data || []); Promise.resolve(supabase.from('messages').update({ read_at: new Date().toISOString() }).eq('sender_id', otherId).eq('recipient_id', user.id).is('read_at', null)).catch(() => {}); if (scroll) setTimeout(() => dmEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 50); }; useEffect(() => { if (!openDm) return undefined; loadDm(openDm.recipient_id); const t = setInterval(() => loadDm(openDm.recipient_id, false), 5000); return () => clearInterval(t); }, [openDm?.recipient_id]); const sendDm = async () => { const content = dmText.trim(); if (!content || !openDm) return; setDmText(''); const { error: e } = await supabase.rpc('send_direct_message', { p_recipient_id: openDm.recipient_id, p_content: content }); if (e) { setError(e.message); setDmText(content); return; } loadDm(openDm.recipient_id); }; const writeTo = (iv) => { setDmMessages([]); setOpenDm(iv); }; const searchInvitees = async (q) => { setInviteQuery(q); const term = q.trim().replace(/[%,()]/g, ''); if (term.length < 2) { setInviteResults([]); return; } const { data } = await supabase.from('profiles').select('id, kliker_name, user_name, profile_photo_url, verified_badge_active') .or(`kliker_name.ilike.%${term}%,user_name.ilike.%${term}%`).neq('id', user.id).limit(6); const memberIds = new Set(members.map((m) => m.user_id)); setInviteResults((data || []).filter((p) => !memberIds.has(p.id))); }; // Sa propre photo (ou le fantôme du profil) sur « Mon espace personnel ». const [myProfile, setMyProfile] = useState(null); useEffect(() => { if (!user?.id) return; Promise.resolve(supabase.from('profiles').select('kliker_name, user_name, profile_photo_url').eq('id', user.id).maybeSingle()) .then(({ data }) => setMyProfile(data || null)).catch(() => {}); }, [user?.id]); // Canaux de l'équipe (onglet Discussion) et panneau de chat à part. const [channels, setChannels] = useState(null); const [openChat, setOpenChat] = useState(null); const [channelForm, setChannelForm] = useState(null); // { id?, name, emoji } const CHANNEL_EMOJIS = ['💬', '🎯', '💡', '📣', '🛍', '📅', '🤝', '📊']; const loadChannels = async (teamId) => { const { data } = await supabase.rpc('get_team_channels', { p_team_id: teamId }); setChannels(data || []); }; const openChannel = (c) => { setOpenChat(c); Promise.resolve(supabase.rpc('mark_channel_read', { p_channel_id: c.channel_id })).catch(() => {}); }; const closeChannel = () => { const c = openChat; setOpenChat(null); if (c) Promise.resolve(supabase.rpc('mark_channel_read', { p_channel_id: c.channel_id })).then(() => current && loadChannels(current.team_id)).catch(() => {}); }; const saveChannel = () => run(async () => { const f = channelForm; if (!f?.name?.trim()) throw new Error('Donne un nom au canal.'); if (f.id) { const { error: e } = await supabase.rpc('rename_team_channel', { p_channel_id: f.id, p_name: f.name.trim(), p_emoji: f.emoji }); if (e) throw e; setOpenChat((c) => (c && c.channel_id === f.id ? { ...c, name: f.name.trim(), emoji: f.emoji } : c)); } else { const { error: e } = await supabase.rpc('create_team_channel', { p_team_id: current.team_id, p_name: f.name.trim(), p_emoji: f.emoji }); if (e) throw e; } setChannelForm(null); loadChannels(current.team_id); }); const [downloadingId, setDownloadingId] = useState(null); const loadTeamCollabs = async (teamId) => { const { data } = await supabase.rpc('get_my_team_collaborations', { p_team_id: teamId }); setTeamCollabs(data || []); }; const downloadStatement = async (contractId) => { if (downloadingId) return; setDownloadingId(contractId); try { const d = await fetchCollaborationDocument(contractId); if (d) await downloadCollaborationStatement(d, user.email); } finally { setDownloadingId(null); } }; const loadDashboard = async (teamId) => { const { data } = await supabase.rpc('get_team_dashboard', { p_team_id: teamId }); setDash(data || null); }; const loadTasks = async (teamId) => { const { data } = await supabase.from('team_tasks').select('id, title, assignee_id, done, created_by, created_at').eq('team_id', teamId).order('done', { ascending: true }).order('created_at', { ascending: false }); setTasks(data || []); }; const loadProjects = async (teamId) => { const { data } = await supabase.rpc('get_team_projects', { p_team_id: teamId }); setProjects(data || []); }; const loadActivity = async (teamId) => { const { data } = await supabase.rpc('get_team_activity', { p_team_id: teamId }); setActivity(data || []); }; const loadMyPurchases = async () => { const { data } = await supabase.from('creator_purchases').select('id, description, amount_usd, status, created_at, team_id, team_project_id').eq('user_id', user.id).neq('type', 'contract').neq('status', 'refunded').order('created_at', { ascending: false }).limit(30); setMyPurchases(data || []); }; useEffect(() => { Promise.resolve(supabase.rpc('get_my_partner_earnings_month')).then(({ data }) => setEarnings(data || null)).catch(() => {}); }, []); // Ouvrir un espace de travail (personnel ou d'équipe) efface le lecteur musique. useEffect(() => { if (showWorkspace) window.dispatchEvent(new CustomEvent('klikklash:closeMusicPlayer')); }, [showWorkspace]); // Se ferme (ainsi que l'espace de travail ouvert dedans) dès qu'un widget // de chat s'ouvre — même règle que les autres panneaux annexes. useEffect(() => { const handler = () => { setShowWorkspace(null); onClose?.(); }; window.addEventListener('klikklash:closeSubPanels', handler); return () => window.removeEventListener('klikklash:closeSubPanels', handler); }, [onClose]); const fetchTeams = async () => { setLoading(true); const { data } = await supabase.rpc('get_my_partner_teams'); setTeams(data || []); setLoading(false); return data || []; }; useEffect(() => { fetchTeams(); }, []); const openTeam = async (t) => { setCurrent(t); setTab('apercu'); setError(''); setDiscussionId(null); setSubTool(null); setDash(null); setGoalDraft(null); setOpenChat(null); setChannels(null); setOpenDm(null); setShowInvites(false); loadDashboard(t.team_id); const { data: memberRows } = await supabase .from('partner_team_members') .select('user_id, joined_at, profiles(kliker_name, user_name, verified_badge_active, profile_photo_url)') .eq('team_id', t.team_id) .order('joined_at', { ascending: true }); setMembers(memberRows || []); const { data: teamRow } = await supabase.from('partner_teams').select('owner_id').eq('id', t.team_id).maybeSingle(); if (teamRow) setCurrent((c) => (c && c.team_id === t.team_id ? { ...c, owner_id: teamRow.owner_id } : c)); const { data: chId } = await supabase.rpc('get_team_discussion_channel', { p_team_id: t.team_id }); Promise.resolve(supabase.rpc('mark_team_notifications_read', { p_team_id: t.team_id })).catch(() => {}); setDiscussionId(chId || null); }; const run = async (fn) => { setSaving(true); setError(''); try { await fn(); } catch (err) { setError(err?.message || 'Une erreur est survenue.'); } finally { setSaving(false); } }; const handleCreate = () => run(async () => { if (!newTeamName.trim()) throw new Error("Choisis un nom pour l'équipe."); const { data: id, error: e } = await supabase.rpc('create_partner_team', { p_name: newTeamName.trim(), p_emoji: newTeamEmoji }); if (e) throw e; setShowCreate(false); setNewTeamName(''); const list = await fetchTeams(); const t = list.find((x) => x.team_id === id); if (t) openTeam(t); }); const handleJoin = () => run(async () => { if (!joinTeamId.trim()) throw new Error("Colle l'identifiant de l'équipe à rejoindre."); const { error: e } = await supabase.rpc('join_partner_team', { p_team_id: joinTeamId.trim() }); if (e) throw e; setShowJoin(false); setJoinTeamId(''); const list = await fetchTeams(); const t = list.find((x) => x.team_id === joinTeamId.trim()); if (t) openTeam(t); }); const askLeave = () => setConfirm({ title: current.is_owner && Number(current.member_count) <= 1 ? "Dissoudre l'équipe ?" : "Quitter l'équipe ?", body: "Tes commissions déjà acquises restent à toi et te seront versées. Tu ne recevras plus de part sur les nouvelles commissions de l'équipe.", label: current.is_owner && Number(current.member_count) <= 1 ? 'Dissoudre' : 'Quitter', action: () => run(async () => { const { error: e } = await supabase.rpc('leave_partner_team', { p_team_id: current.team_id }); if (e) throw e; setCurrent(null); await fetchTeams(); }), }); const askKick = (m) => setConfirm({ title: `Retirer ${m.profiles?.kliker_name || m.profiles?.user_name || 'ce membre'} ?`, body: "Ses commissions déjà acquises restent à lui. Il ne recevra plus de part sur les nouvelles commissions de l'équipe.", label: 'Retirer', action: () => run(async () => { const { error: e } = await supabase.rpc('kick_partner_team_member', { p_user_id: m.user_id, p_team_id: current.team_id }); if (e) throw e; await fetchTeams(); openTeam({ ...current, member_count: Number(current.member_count) - 1 }); }), }); const copy = (key, text) => { navigator.clipboard.writeText(text); setCopied(key); setTimeout(() => setCopied(''), 1500); }; const teamLink = current && personalReferralCode ? `${window.location.origin}/?partner=${personalReferralCode}&channel=${current.team_channel}` : ''; const fmtTime = (d) => { if (!d) return ''; const dt = new Date(d); const now = new Date(); return dt.toDateString() === now.toDateString() ? dt.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }); }; const n = Math.max(1, Number(current?.member_count || members.length || 1)); const yourShare = (15 + 15 / n).toFixed(2).replace('.', ','); const otherShare = (15 / n).toFixed(2).replace('.', ','); const G = { blue: 'linear-gradient(135deg,#0a84ff,#5e5ce6)', green: 'linear-gradient(135deg,#34c759,#1a8a3f)', orange: 'linear-gradient(135deg,#ff9f0a,#ff375f)', gold: 'linear-gradient(135deg,#ffd60a,#ff9f0a)', purple: 'linear-gradient(135deg,#bf5af2,#5e5ce6)', teal: 'linear-gradient(135deg,#64d2ff,#0a84ff)', pink: 'linear-gradient(135deg,#ff6482,#ff375f)', gray: 'linear-gradient(135deg,#8e8e93,#48484a)', }; const gradIcon = (emoji, bg, size = 28) => ( {emoji} ); const card = { background: '#fff', borderRadius: 18, padding: 13, marginBottom: 10, border: '1px solid rgba(255,255,255,0.6)' }; const label = { fontSize: 12, fontWeight: 700, color: 'rgba(255,255,255,0.5)', textTransform: 'uppercase', letterSpacing: '.03em', margin: '14px 2px 8px' }; const pillBtn = (bg, color) => ({ width: '100%', border: 'none', borderRadius: 999, padding: 12, fontSize: 14, fontWeight: 700, cursor: 'pointer', background: bg, color, fontFamily: 'inherit' }); const linkRow = (key, text) => (
{text || 'Lien indisponible'}
); // ----- Contenu de l'accueil ----- const home = ( <> {earnings && ( )}
Mes équipes · {teams.length} sur 3
{teams.length === 0 && (
Crée une équipe pour démarcher à plusieurs. Chaque membre a son propre lien d'équipe : sur chaque commission, 50 % vont à celui qui a amené le filleul et 50 % sont partagés à parts égales entre tous les membres.
)} {teams.length > 0 && (
{teams.map((t, i) => ( ))}
)} {teams.length < 3 && !showCreate && !showJoin && ( <> )} {showCreate && (
Nouvelle équipe
{TEAM_EMOJIS.map((e) => ( ))}
setNewTeamName(e.target.value)} placeholder="Nom de l'équipe" style={{ width: '100%', boxSizing: 'border-box', padding: '11px 12px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 14, outline: 'none', marginBottom: 8 }} />
Règle de l'équipe, la même pour tous : 50 % pour celui qui amène le filleul, 50 % partagés à parts égales entre les membres.
)} {showJoin && (
Rejoindre une équipe
setJoinTeamId(e.target.value)} placeholder="Identifiant de l'équipe" style={{ width: '100%', boxSizing: 'border-box', padding: '11px 12px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 14, outline: 'none', marginBottom: 10 }} />
)} {teams.length >= 3 &&
Tu fais partie de 3 équipes, le maximum.
} ); // ----- Contenu d'une équipe ----- const teamTabs = current && ( <> {tab === 'apercu' && ( <>
{gradIcon(current.emoji || '🚀', G.blue, 38)} {current.name} {current.is_owner ? 'Créateur' : 'Membre'}
Commissions de l'équipe ce mois-ci
{Number(dash?.gains_month || 0).toFixed(2).replace('.', ',')} $
dont ta part : {Number(dash?.my_gains_month || 0).toFixed(2).replace('.', ',')} $
{[['Filleuls du mois', dash?.referrals_month ?? '-'], ['Membres', n], ['Projets', dash?.projects ?? '-'], ['Tâches en cours', dash?.open_tasks ?? '-']].map(([l, v]) => (
{l}
{v}
))}
{gradIcon('🎯', G.orange)} Objectif du mois {dash?.goal ? {dash.referrals_month} / {dash.goal} filleuls : null} {current.owner_id === user.id && goalDraft === null && ( )}
{goalDraft !== null ? (
setGoalDraft(e.target.value)} placeholder="Nombre de filleuls" style={{ flex: 1, padding: '9px 11px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13.5, outline: 'none' }} />
) : dash?.goal ? (
) : (
{current.owner_id === user.id ? "Fixe un nombre de filleuls à atteindre ce mois-ci : il motive toute l'équipe." : "Le créateur n'a pas encore fixé d'objectif."}
)}
{(dash?.leaderboard || []).length > 1 && (
{gradIcon('🏆', G.gold)} Classement du mois {dash.leaderboard.map((m, i) => (
{i + 1}
{m.name} {m.referrals} filleul{Number(m.referrals) > 1 ? 's' : ''}
))}
)}
{gradIcon(current.emoji || '🚀', G.blue)} Ton lien d'équipe
Pour les actions menées ensemble : une campagne d'équipe, un événement, un démarchage commun. Quand une personne arrivée par ce lien paie, la commission est partagée : 50 % pour toi qui l'as amenée, 50 % répartis entre tous les membres.
{linkRow('equipe', teamLink)}
⚠️ Les personnes qui arrivent par ce lien deviennent des utilisateurs de KlikKlash, pas des membres de ton équipe. Pour faire entrer un partenaire dans l'équipe, utilise le lien d'invitation (onglet Membres).
Ton lien personnel, à 100 % pour toi, reste dans ton Espace Partenaire.
{n <= 1 ? ( <>Tant que tu es seul dans l'équipe, toute la commission te revient. L'intérêt du lien d'équipe arrive avec les membres : chacun touche une part quand l'un de vous amène quelqu'un. Invite des partenaires depuis l'onglet Membres. ) : ( <>Exemple avec {n} membres, sur 30 $ de commission :
· quand tu amènes quelqu'un par ce lien, tu reçois {yourShare} $, et chaque autre membre {otherShare} $ ;
· quand un autre membre amène quelqu'un par son lien d'équipe, tu reçois {otherShare} $ sans rien faire. )}
)} {tab === 'discussion' && ( channels === null ? (
Chargement des canaux…
) : ( <>
Canaux de l'équipe · {channels.length}
{channels.map((c) => ( ))}
Tous les membres voient tous les canaux. L'auteur d'un canal, ou le créateur de l'équipe, peut le renommer ou le supprimer.
) )} {tab === 'membres' && ( <>
{members.map((m, i) => (
{m.profiles?.kliker_name || m.profiles?.user_name || 'Membre'} {m.profiles?.verified_badge_active && } {m.user_id === current.owner_id && Créateur} {current.is_owner && m.user_id !== user.id && ( )}
))}
+ Inviter quelqu'un = 10 ? 'rgba(255,59,48,0.1)' : '#f2f2f7', color: n >= 10 ? '#ff3b30' : '#6e6e73', flexShrink: 0 }}>{n} / 10 membres
Cherche la personne : elle reçoit l'invitation dans ses messages. Si elle n'est pas encore partenaire, elle pourra le devenir, puis rejoindre l'équipe.
{n >= 10 ? (
Ton équipe est complète : 10 membres au plus. Pour inviter quelqu'un, un membre doit d'abord quitter l'équipe.
) : searchInvitees(e.target.value)} placeholder="Nom ou pseudo" style={{ width: '100%', boxSizing: 'border-box', padding: '10px 12px', borderRadius: 12, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13.5, outline: 'none', margin: '10px 0 4px', background: '#f2f2f7' }} />} {n < 10 && inviteResults.map((p) => { const sent = invitedIds.has(p.id); return (
{p.kliker_name || p.user_name}
); })}
{invitations.length > 0 && (
)} )} {tab === 'outils' && !subTool && (
{[ { e: '✅', g: G.green, t: 'Tâches', h: 'Qui démarche quelle marque, qui fait quoi', go: () => { setSubTool('taches'); loadTasks(current.team_id); } }, { e: '📁', g: G.purple, t: 'Projets et budget', h: 'Investissements et retour sur investissement', go: () => { setSubTool('projets'); loadProjects(current.team_id); } }, { e: '📊', g: G.teal, t: 'Activité', h: "Tout ce que fait l'équipe", go: () => { setSubTool('activite'); loadActivity(current.team_id); } }, { e: '📄', g: G.gray, t: 'Collaborations et relevés', h: 'Ta part de chaque collaboration et ton relevé', go: () => { setSubTool('relevés'); loadTeamCollabs(current.team_id); } }, { e: '💬', g: G.blue, t: 'Messages clients', h: "Conversations de l'équipe avec ses clients", go: () => setShowWorkspace({ scopeType: 'team', scopeId: current.team_id, spaceName: current.name }) }, { e: '🤝', g: G.orange, t: "Apport d'affaires", h: "Présente des besoins au nom de l'équipe", go: () => window.dispatchEvent(new CustomEvent('klikklash:openAirFlash', { detail: { teamId: current.team_id } })) }, { e: '💼', g: G.gold, t: 'AirDeal', h: "Réponds aux offres au nom de l'équipe", go: () => window.dispatchEvent(new CustomEvent('klikklash:openAirDealPartner', { detail: { teamId: current.team_id } })) }, ].map((x) => ( ))}
)} {tab === 'outils' && subTool && ( )} {tab === 'outils' && subTool === 'taches' && ( <>
setNewTask(e.target.value)} placeholder="Nouvelle tâche (ex. démarcher 3 boutiques)" style={{ width: '100%', boxSizing: 'border-box', padding: '10px 11px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13.5, outline: 'none', marginBottom: 8 }} />
{tasks.length === 0 ? (
Aucune tâche pour l'instant.
) : (
{tasks.map((tk, i) => { const who = members.find((m) => m.user_id === tk.assignee_id); return (
{tk.title} {who ? ( {who.profiles?.kliker_name || who.profiles?.user_name} ) : Toute l'équipe} {(tk.created_by === user.id || current.owner_id === user.id) && ( )}
); })}
)} )} {tab === 'outils' && subTool === 'projets' && ( <>
setNewProject(e.target.value)} placeholder="Nouveau projet (ex. Campagne commerces d'octobre)" style={{ flex: 1, minWidth: 0, padding: '10px 11px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13.5, outline: 'none' }} />
Chaque projet a son propre lien, pour savoir exactement ce qu'il rapporte. Les commissions restent partagées selon la règle de l'équipe (50 % pour celui qui amène le filleul, 50 % partagés), jamais selon l'argent investi.
{projects.map((pr) => { const roi = Number(pr.invested) > 0 ? Math.round(((Number(pr.returned) - Number(pr.invested)) / Number(pr.invested)) * 100) : null; const projLink = personalReferralCode ? `${window.location.origin}/?partner=${personalReferralCode}&channel=${pr.channel}` : ''; return (
{gradIcon('📁', G.purple)}{pr.name}
{[['Investi', `${Number(pr.invested).toFixed(2).replace('.', ',')} $`], ['Rapporté', `${Number(pr.returned).toFixed(2).replace('.', ',')} $`], ['Retour', roi === null ? '-' : `${roi > 0 ? '+' : ''}${roi} %`]].map(([l, v]) => (
{l}
= 0 ? '#1a8a3f' : '#ff3b30') : '#1d1d1f' }}>{v}
))}
{pr.referrals} filleul{Number(pr.referrals) > 1 ? 's' : ''}{Number(pr.invested) > 0 ? ` · tu as investi ${Number(pr.my_invested).toFixed(2).replace('.', ',')} $ (${Math.round((Number(pr.my_invested) / Number(pr.invested)) * 100)} % du total)` : ''}{Number(pr.invested_pending) > 0 ? ` · ${Number(pr.invested_pending).toFixed(2).replace('.', ',')} $ en attente de paiement` : ''}
{linkRow(`p-${pr.id}`, projLink)}
); })}
{gradIcon('💰', G.green)} Rattacher un de tes achats
Tu paies tes achats seul : en les rattachant, ils apparaissent dans le budget de l'équipe, mais personne ne te les rembourse.
{myPurchases === null ? ( ) : ( <> {myPurchases.length === 0 &&
Aucun achat à rattacher.
} {myPurchases.map((pu) => { const here = pu.team_id === current.team_id; return (
{pu.description} · {Number(pu.amount_usd).toFixed(2).replace('.', ',')} ${pu.status === 'pending' ? ' (en attente)' : ''}
); })} )}
)} {tab === 'outils' && subTool === 'relevés' && ( teamCollabs.length === 0 ? (
Aucune collaboration d'équipe clôturée pour l'instant. Réponds à une offre AirDeal « pour l'équipe » : à la clôture, chaque membre reçoit sa part et son relevé.
) : (
{teamCollabs.map((c, i) => (
« {c.title} » {fmtTime(c.settled_at)}
Menée par {c.lead_name} · {Number(c.net_usd || 0).toFixed(2).replace('.', ',')} $ nets pour l'équipe
Ta part : {Number(c.my_payout || 0).toFixed(2).replace('.', ',')} $ {Number(c.my_payout || 0) > 0 && ( )}
))}
) )} {tab === 'outils' && subTool === 'activite' && ( activity.length === 0 ? (
Aucune activité pour l'instant.
) : (
{activity.map((a, i) => (
{{ filleul: '🎉', commission: '💵', tache: '✅', membre: '👋', projet: '📁', achat: '💰' }[a.kind] || '•'} {a.label} {fmtTime(a.at)}
))}
) )} ); return createPortal(
{!showWorkspace && (
e.stopPropagation()} style={{ background: 'rgba(20,20,22,0.55)', backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)', border: '1px solid rgba(255,255,255,0.12)', borderBottom: 'none', width: '100%', maxWidth: 420, borderRadius: '20px 20px 0 0', padding: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', boxSizing: 'border-box', maxHeight: '85vh', height: current ? '85vh' : undefined, isolation: 'isolate', boxShadow: '0 -8px 40px rgba(0,0,0,0.25)', visibility: openChat || openDm || showInvites ? 'hidden' : 'visible', pointerEvents: openChat || openDm || showInvites ? 'none' : 'auto' }}> {/* En-tête fixe : seul le contenu défile en dessous. */}
{current ? ( ) : gradIcon('💰', G.gold, 30)}{current && gradIcon(current.emoji || '🚀', G.blue, 30)} {current ? current.name : 'Espace de travail'}
{current && (
{[['apercu', 'Aperçu'], ['discussion', 'Discussion'], ['membres', 'Membres'], ['outils', 'Outils']].map(([k, l]) => ( ))}
)}
{loading && !current ? (
{[0, 1, 2].map((i) => (
))}
) : current ? teamTabs : home} {error &&
{error}
}
)} {showInvites && current && (
e.stopPropagation()} style={{ pointerEvents: openDm ? 'none' : 'auto', background: 'rgba(20,20,22,0.55)', backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)', border: '1px solid rgba(255,255,255,0.12)', borderBottom: 'none', width: '100%', maxWidth: 420, height: '85vh', borderRadius: '20px 20px 0 0', display: 'flex', flexDirection: 'column', overflow: 'hidden', boxShadow: '0 -8px 40px rgba(0,0,0,0.25)' }}>
{gradIcon('✉️', G.teal, 34)} Invitations envoyées {current.emoji || '🚀'} {current.name} · {invitations.length} invitation{invitations.length > 1 ? 's' : ''}
{(
{[['all', 'Toutes'], ['waiting', 'En attente'], ['pending_partner', 'Partenaire'], ['joined', 'Rejoint']].map(([k, l]) => { const n = k === 'all' ? invitations.length : invitations.filter((iv) => iv.status === k).length; return ; })}
)}
{invitations.filter((iv) => inviteFilter === 'all' || iv.status === inviteFilter).map((iv) => { const st = { joined: ["A rejoint l'équipe", '#1a8a3f', 'rgba(52,199,89,0.12)'], pending_partner: ['Devient partenaire', '#5e5ce6', 'rgba(94,92,230,0.12)'], waiting: ['En attente', '#b26a00', 'rgba(255,159,10,0.14)'] }[iv.status] || ['En attente', '#b26a00', 'rgba(255,159,10,0.14)']; return (
{iv.recipient_name} {st[0]} par {iv.inviter_name} · {fmtTime(iv.sent_at)}
); })} {invitations.filter((iv) => inviteFilter === 'all' || iv.status === inviteFilter).length === 0 && (
Aucune invitation dans cette catégorie.
)}
)} {openDm && current && (
e.stopPropagation()} style={{ pointerEvents: 'auto', background: 'rgba(20,20,22,0.55)', backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)', border: '1px solid rgba(255,255,255,0.12)', borderBottom: 'none', width: '100%', maxWidth: 420, height: '85vh', borderRadius: '20px 20px 0 0', display: 'flex', flexDirection: 'column', overflow: 'hidden', boxShadow: '0 -8px 40px rgba(0,0,0,0.25)' }}>
{openDm.recipient_name} Invitation · équipe {current.emoji || '🚀'} {current.name}
{dmMessages.length === 0 && (
💬

Écris à {openDm.recipient_name} : il te répondra depuis sa messagerie.

)} {dmMessages.map((m) => { const mine = m.sender_id === user.id; return (
{m.content}
{new Date(m.created_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} {mine && }
); })}