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'}
text && copy(key, text)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#007aff', fontSize: 12.5, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
{copied === key ? <> Copié> : <> Copier>}
);
// ----- Contenu de l'accueil -----
const home = (
<>
setShowWorkspace({ scopeType: 'personal', scopeId: user.id, spaceName: 'Mon espace personnel' })} style={{ ...card, width: '100%', border: 'none', display: 'flex', alignItems: 'center', gap: 11, cursor: 'pointer', textAlign: 'left', background: 'linear-gradient(135deg,#eef6ff,#fff)', fontFamily: 'inherit' }}>
Mon espace personnel
Canaux, messages clients et outils
{earnings && (
setShowEarnings((v) => !v)} style={{ ...card, width: '100%', border: 'none', textAlign: 'left', cursor: 'pointer', fontFamily: 'inherit' }}>
💵 Tes gains ce mois-ci
{(Number(earnings.personal || 0) + Number(earnings.teams_total || 0)).toFixed(2).replace('.', ',')} $
{showEarnings && (
👤 Ton lien perso : {Number(earnings.personal || 0).toFixed(2).replace('.', ',')} $
{(earnings.teams || []).map((t) => {t.emoji} {t.name} : {Number(t.amount || 0).toFixed(2).replace('.', ',')} $ )}
Commissions acquises ce mois-ci, versées selon les conditions du Programme Partenaire.
)}
)}
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) => (
openTeam(t)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 11, padding: '11px 12px', background: 'none', border: 'none', borderTop: i ? '0.5px solid rgba(0,0,0,0.07)' : 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }}>
{t.emoji || '🚀'}
{t.name}
{t.is_owner && }
{t.notifications_muted && 🔕 }
{fmtTime(t.last_at)}
{Number(t.unread) > 0 && {t.unread} }
{t.last_message ? (
{t.last_sender || 'Un membre'}
:
{t.last_message}
) : (
{t.member_count} membre{Number(t.member_count) > 1 ? 's' : ''} · aucun message pour l'instant
)}
))}
)}
{teams.length < 3 && !showCreate && !showJoin && (
<>
{ setShowCreate(true); setError(''); }} style={pillBtn('rgba(0,122,255,0.1)', '#fff')}>+ Créer une équipe
{ setShowJoin(true); setError(''); }} style={{ ...pillBtn('#e5e5ea', '#1d1d1f'), marginTop: 8 }}>Rejoindre avec un identifiant
>
)}
{showCreate && (
Nouvelle équipe
{TEAM_EMOJIS.map((e) => (
setNewTeamEmoji(e)} style={{ width: 38, height: 38, borderRadius: 10, border: 'none', cursor: 'pointer', fontSize: 19, background: newTeamEmoji === e ? '#e8f2ff' : '#f2f2f7', outline: newTeamEmoji === e ? '1.5px solid #007aff' : 'none' }}>{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.
{saving ? '…' : "Créer l'équipe"}
setShowCreate(false)} style={{ ...pillBtn('transparent', '#8e8e93'), marginTop: 4 }}>Annuler
)}
{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 }} />
{saving ? '…' : 'Rejoindre'}
setShowJoin(false)} style={{ ...pillBtn('transparent', '#8e8e93'), marginTop: 4 }}>Annuler
)}
{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]) => (
))}
{gradIcon('🎯', G.orange)} Objectif du mois
{dash?.goal ? {dash.referrals_month} / {dash.goal} filleuls : null}
{current.owner_id === user.id && goalDraft === null && (
setGoalDraft(String(dash?.goal || ''))} style={{ background: 'none', border: 'none', color: '#007aff', fontSize: 12, fontWeight: 700, cursor: 'pointer' }}>{dash?.goal ? 'Modifier' : 'Fixer'}
)}
{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' }} />
run(async () => { const g = parseInt(goalDraft, 10); const { error: e } = await supabase.rpc('set_team_monthly_goal', { p_team_id: current.team_id, p_goal: Number.isFinite(g) ? g : null }); if (e) throw e; setGoalDraft(null); loadDashboard(current.team_id); })} style={{ border: 'none', borderRadius: 999, padding: '0 14px', background: '#007aff', color: '#fff', fontWeight: 700, cursor: 'pointer' }}>OK
setGoalDraft(null)} style={{ border: 'none', borderRadius: 999, padding: '0 12px', background: '#e5e5ea', color: '#1d1d1f', fontWeight: 600, cursor: 'pointer' }}>Annuler
) : 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) => (
openChannel(c)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 12, background: '#fff', border: 'none', borderRadius: 18, padding: 13, marginBottom: 9, cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
{c.emoji || '💬'}
{c.name}
{c.is_main && principal }
{c.last_message ? (
{c.last_sender || 'Un membre'}
:
{c.last_message}
) : (
Aucun message pour l'instant
)}
{fmtTime(c.last_at)}
{Number(c.unread) > 0 && {c.unread} }
))}
setChannelForm({ name: '', emoji: '💬' })} style={{ ...pillBtn('rgba(0,122,255,0.1)', '#fff'), marginTop: 4 }}>+ Nouveau canal
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 && (
askKick(m)} title="Retirer de l'équipe" style={{ background: 'rgba(255,59,48,0.08)', border: 'none', borderRadius: 8, cursor: 'pointer', padding: 6, flexShrink: 0 }}>
)}
))}
+ 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}
run(async () => { const { error: e } = await supabase.rpc('send_team_invite', { p_team_id: current.team_id, p_recipient_id: p.id }); if (e) throw e; setInvitedIds((prev) => new Set(prev).add(p.id)); loadInvitations(current.team_id); })} style={{ border: 'none', borderRadius: 999, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: sent ? 'default' : 'pointer', background: sent ? '#e5e5ea' : '#007aff', color: sent ? '#8e8e93' : '#fff', flexShrink: 0, fontFamily: 'inherit' }}>{sent ? 'Envoyée ✓' : 'Inviter'}
);
})}
{invitations.length > 0 && (
{ setInviteFilter('all'); setShowInvites(true); }} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 8, background: 'none', border: 'none', padding: 0, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}>
{gradIcon('✉️', G.teal)}
Invitations envoyées · {invitations.length}
{invitations.some((iv) => iv.status !== 'joined') && {invitations.filter((iv) => iv.status !== 'joined').length} en cours }
)}
run(async () => { const muted = !current.notifications_muted; const { error: e } = await supabase.rpc('set_team_notifications_muted', { p_team_id: current.team_id, p_muted: muted }); if (e) throw e; setCurrent((c) => ({ ...c, notifications_muted: muted })); })} style={{ ...card, width: '100%', border: 'none', display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' }}>
{gradIcon(current.notifications_muted ? '🔕' : '🔔', current.notifications_muted ? G.gray : G.pink, 32)}
Notifications de l'équipe
Messages, tâches confiées et objectif atteint
{current.is_owner && n <= 1 ? "Dissoudre l'équipe" : "Quitter l'équipe"}
>
)}
{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) => (
{gradIcon(x.e, x.g, 38)}
{x.t}
{x.h}
))}
)}
{tab === 'outils' && subTool && (
{ setSubTool(null); setMyPurchases(null); }} style={{ border: 'none', borderRadius: 999, padding: '6px 12px', fontSize: 12, fontWeight: 700, background: '#e5e5ea', color: '#1d1d1f', cursor: 'pointer', marginBottom: 10, fontFamily: 'inherit' }}>‹ Outils
)}
{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 }} />
setNewTaskAssignee(e.target.value)} style={{ flex: 1, padding: '9px 10px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13, background: '#fff' }}>
Pour toute l'équipe
{members.map((m) => {m.profiles?.kliker_name || m.profiles?.user_name || 'Membre'} )}
run(async () => { const { error: e } = await supabase.rpc('create_team_task', { p_team_id: current.team_id, p_title: newTask, p_assignee: newTaskAssignee || null }); if (e) throw e; setNewTask(''); setNewTaskAssignee(''); loadTasks(current.team_id); loadDashboard(current.team_id); })} style={{ border: 'none', borderRadius: 999, padding: '0 16px', background: '#007aff', color: '#fff', fontWeight: 700, cursor: 'pointer' }}>Ajouter
{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 (
run(async () => { const { error: e } = await supabase.rpc('toggle_team_task', { p_task_id: tk.id }); if (e) throw e; loadTasks(current.team_id); loadDashboard(current.team_id); })} aria-label="Terminer" style={{ width: 22, height: 22, borderRadius: 7, flexShrink: 0, cursor: 'pointer', border: tk.done ? 'none' : '2px solid #c7c7cc', background: tk.done ? '#34c759' : 'transparent', color: '#fff', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{tk.done ? '✓' : ''}
{tk.title}
{who ? (
{who.profiles?.kliker_name || who.profiles?.user_name}
) : Toute l'équipe }
{(tk.created_by === user.id || current.owner_id === user.id) && (
run(async () => { const { error: e } = await supabase.rpc('delete_team_task', { p_task_id: tk.id }); if (e) throw e; loadTasks(current.team_id); loadDashboard(current.team_id); })} aria-label="Supprimer" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}>
)}
);
})}
)}
>
)}
{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' }} />
run(async () => { const { error: e } = await supabase.rpc('create_team_project', { p_team_id: current.team_id, p_name: newProject }); if (e) throw e; setNewProject(''); loadProjects(current.team_id); loadDashboard(current.team_id); })} style={{ border: 'none', borderRadius: 999, padding: '0 16px', background: '#007aff', color: '#fff', fontWeight: 700, cursor: 'pointer' }}>Créer
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 ? (
Choisir un achat
) : (
<>
setAttachProject(e.target.value)} style={{ width: '100%', padding: '9px 10px', borderRadius: 10, border: '0.5px solid rgba(0,0,0,0.12)', fontSize: 13, background: '#fff', margin: '10px 0 8px' }}>
Sans projet précis
{projects.map((pr) => {pr.name} )}
{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)' : ''}
run(async () => { const { error: e } = await supabase.rpc('attach_purchase_to_team', { p_purchase_id: pu.id, p_team_id: here ? null : current.team_id, p_project_id: here ? null : (attachProject || null) }); if (e) throw e; loadMyPurchases(); loadProjects(current.team_id); })} style={{ border: 'none', borderRadius: 999, padding: '6px 11px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer', background: here ? '#e5e5ea' : '#007aff', color: here ? '#1d1d1f' : '#fff', flexShrink: 0 }}>{here ? 'Détacher' : 'Rattacher'}
);
})}
>
)}
>
)}
{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 && (
downloadStatement(c.contract_id)} style={{ border: 'none', borderRadius: 999, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: 'pointer', background: 'rgba(0,122,255,0.1)', color: '#007aff', fontFamily: 'inherit' }}>{downloadingId === c.contract_id ? '…' : '🧾 Mon relevé'}
)}
))}
)
)}
{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 ? (
{ setCurrent(null); setError(''); fetchTeams(); }} aria-label="Retour" style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(255,255,255,0.12)', color: '#fff', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
) : 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]) => (
{ setTab(k); setSubTool(null); if (k === 'discussion') loadChannels(current.team_id); if (k === 'membres') loadInvitations(current.team_id); }} style={{ flex: 1, border: 'none', borderRadius: 11, padding: '8px 0', fontSize: 12.5, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', background: tab === k ? 'linear-gradient(135deg,#0a84ff,#007aff)' : 'transparent', color: tab === k ? '#fff' : 'rgba(255,255,255,0.6)', boxShadow: tab === k ? '0 4px 12px rgba(0,122,255,0.35)' : 'none' }}>{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)' }}>
setShowInvites(false)} aria-label="Retour" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, marginLeft: -4, flexShrink: 0 }}>
{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 setInviteFilter(k)} style={{ flexShrink: 0, border: 'none', borderRadius: 999, padding: '6px 11px', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit', background: inviteFilter === k ? '#007aff' : '#f2f2f7', color: inviteFilter === k ? '#fff' : '#3a3a3c' }}>{l} · {n} ;
})}
)}
{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)}
writeTo(iv)} style={{ border: 'none', borderRadius: 999, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: 'pointer', background: 'rgba(0,122,255,0.1)', color: '#007aff', flexShrink: 0, fontFamily: 'inherit' }}>Écrire
);
})}
{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)' }}>
{ setOpenDm(null); loadInvitations(current.team_id); }} aria-label="Retour" style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', padding: 0, marginLeft: -4, flexShrink: 0 }}>
{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 && }
);
})}
)}
{openChat && current && !showWorkspace && (
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)' }}>
{/* En-tête fixe du chat */}
{gradIcon(openChat.emoji || '💬', G.blue, 34)}
{openChat.name}
{current.emoji || '🚀'} {current.name} · {n} membre{n > 1 ? 's' : ''}
{!openChat.is_main && (openChat.created_by === user.id || current.owner_id === user.id) && (
setChannelForm({ id: openChat.channel_id, name: openChat.name, emoji: openChat.emoji || '💬' })} aria-label="Modifier le canal" style={{ width: 30, height: 30, borderRadius: '50%', background: 'rgba(255,255,255,0.12)', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 14, flexShrink: 0 }}>⋯
)}
)}
{channelForm && (
{channelForm.id ? 'Modifier le canal' : 'Nouveau canal'}
setChannelForm((f) => ({ ...f, name: e.target.value }))} maxLength={40} placeholder="Nom du canal (ex. Prospection)" style={{ width: '100%', boxSizing: 'border-box', padding: '10px 12px', borderRadius: 10, border: 'none', fontSize: 14, margin: '12px 0 10px', outline: 'none' }} />
{CHANNEL_EMOJIS.map((e) => (
setChannelForm((f) => ({ ...f, emoji: e }))} style={{ width: 34, height: 34, borderRadius: 9, border: 'none', cursor: 'pointer', fontSize: 17, background: channelForm.emoji === e ? 'rgba(10,132,255,0.3)' : 'rgba(255,255,255,0.12)', outline: channelForm.emoji === e ? '1.5px solid #0a84ff' : 'none' }}>{e}
))}
setChannelForm(null)} style={amcGlassCancel}>Annuler
{channelForm.id ? 'Enregistrer' : 'Créer'}
{channelForm.id && (
{ const id = channelForm.id; const nm = channelForm.name; setChannelForm(null); setConfirm({ title: `Supprimer « ${nm} » ?`, body: 'Le canal et tous ses messages seront supprimés pour toute l\'équipe.', label: 'Supprimer', action: () => run(async () => { const { error: e } = await supabase.rpc('delete_team_channel', { p_channel_id: id }); if (e) throw e; setOpenChat(null); loadChannels(current.team_id); }) }); }} style={{ ...amcGlassDanger, width: '100%', flex: 'none', marginTop: 10 }}> Supprimer le canal
)}
{error &&
{error}
}
)}
{showWorkspace && (
setShowWorkspace(null)}
/>
)}
{confirm && (
{confirm.title}
{confirm.body}
setConfirm(null)} style={amcGlassCancel}>Annuler
{ const a = confirm.action; setConfirm(null); a(); }} style={amcGlassDanger}>{confirm.label === 'Supprimer' && } {confirm.label}
)}
,
document.body
);
};
export default TeamManagementModal;
export const FloatingLogo = ({ size = 38 }) => (
);
export const FloatingTeamButton = ({ hidden, onOpenPartnerSpace, onOpenAirNote, onOpenContracts, onOpenAirBloom, onOpenBoutique }) => {
const { user } = useAuth();
const [showShortcuts, setShowShortcuts] = useState(false);
const [showModal, setShowModal] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [personalReferralCode, setPersonalReferralCode] = useState(null);
const [totalUnread, setTotalUnread] = useState(0);
const [prefVisible, setPrefVisible] = useState(true);
// La position déplacée est mémorisée dans le navigateur. Elle est toujours
// recalée dans l'écran visible : une position prise sur ordinateur (ex.
// right: 1200px) mettait sinon le bouton hors de l'écran d'un téléphone.
const clampFloatingPos = (p) => {
const btnSize = 46;
const maxRight = Math.max(4, window.innerWidth - btnSize - 4);
const maxBottom = Math.max(4, window.innerHeight - btnSize - 4);
const right = Number.isFinite(p?.right) ? p.right : 16;
const bottom = Number.isFinite(p?.bottom) ? p.bottom : 84;
return { right: Math.min(Math.max(right, 4), maxRight), bottom: Math.min(Math.max(bottom, 4), maxBottom) };
};
const [pos, setPos] = useState(() => {
try {
const saved = localStorage.getItem('kk_floating_team_btn_pos');
return clampFloatingPos(saved ? JSON.parse(saved) : null);
} catch (e) {
return clampFloatingPos(null);
}
});
useEffect(() => {
const onResize = () => setPos((prev) => clampFloatingPos(prev));
window.addEventListener('resize', onResize);
window.addEventListener('orientationchange', onResize);
return () => { window.removeEventListener('resize', onResize); window.removeEventListener('orientationchange', onResize); };
}, []);
const dragState = useRef({ dragging: false, moved: false, startX: 0, startY: 0 });
useEffect(() => {
if (!user?.id) return;
supabase.from('affiliates').select('referral_code').eq('user_id', user.id).maybeSingle().then(({ data }) => {
setPersonalReferralCode(data?.referral_code || null);
});
}, [user?.id]);
// Préférence "afficher le bouton flottant" (Réglages > Raccourcis).
useEffect(() => {
if (!user?.id) return;
supabase.from('profiles').select('shortcuts_button_visible').eq('id', user.id).maybeSingle()
.then(({ data }) => setPrefVisible(data?.shortcuts_button_visible !== false));
const handler = (e) => setPrefVisible(e.detail?.visible !== false);
window.addEventListener('klikklash:shortcutsVisibilityChanged', handler);
return () => window.removeEventListener('klikklash:shortcutsVisibilityChanged', handler);
}, [user?.id]);
// Bulle totale sur le bouton flottant — somme des notifications non lues
// (tous types confondus) + messages non lus. Rafraîchi à chaque ouverture
// du panneau raccourcis via l'event ci-dessous.
useEffect(() => {
if (!user?.id) return;
const fetchTotal = () => {
Promise.all([
supabase.from('notifications').select('id', { count: 'exact', head: true }).eq('user_id', user.id).eq('is_read', false),
supabase.from('messages').select('id', { count: 'exact', head: true }).eq('recipient_id', user.id).is('read_at', null),
]).then(([notifRes, msgRes]) => {
setTotalUnread((notifRes.count || 0) + (msgRes.count || 0));
});
};
fetchTotal();
window.addEventListener('klikklash:shortcutsNotifRefresh', fetchTotal);
return () => window.removeEventListener('klikklash:shortcutsNotifRefresh', fetchTotal);
}, [user?.id]);
// Réouverture automatique du panneau Raccourcis/Notes quand on revient
// de la recherche globale (bouton retour) après l'avoir ouverte depuis ici.
useEffect(() => {
const handler = () => setShowShortcuts(true);
window.addEventListener('klikklash:reopenTasksPanel', handler);
return () => window.removeEventListener('klikklash:reopenTasksPanel', handler);
}, []);
const handlePointerDown = (e) => {
dragState.current = { dragging: true, moved: false, startX: e.clientX, startY: e.clientY };
e.currentTarget.setPointerCapture(e.pointerId);
};
const handlePointerMove = (e) => {
if (!dragState.current.dragging) return;
const dx = e.clientX - dragState.current.startX;
const dy = e.clientY - dragState.current.startY;
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) dragState.current.moved = true;
if (!dragState.current.moved) return;
setPos((prev) => {
const btnSize = 46;
const newRight = Math.min(Math.max((prev.right ?? 16) - dx, 4), window.innerWidth - btnSize - 4);
const newBottom = Math.min(Math.max((prev.bottom ?? 84) - dy, 4), window.innerHeight - btnSize - 4);
return { bottom: newBottom, right: newRight };
});
dragState.current.startX = e.clientX;
dragState.current.startY = e.clientY;
};
const handlePointerUp = () => {
if (dragState.current.moved) {
localStorage.setItem('kk_floating_team_btn_pos', JSON.stringify(pos));
} else {
setShowShortcuts(true);
}
dragState.current = { dragging: false, moved: false, startX: 0, startY: 0 };
};
if (hidden || !prefVisible) return null;
return (
<>
{totalUnread > 0 && (
)}
{showShortcuts && (
{ setShowShortcuts(false); window.dispatchEvent(new CustomEvent('klikklash:shortcutsNotifRefresh')); }}
onOpenPartnerSpace={() => { setShowShortcuts(false); onOpenPartnerSpace?.(); }}
onOpenWorkspace={() => { setShowShortcuts(false); window.dispatchEvent(new CustomEvent('klikklash:closeKlashReaders')); window.dispatchEvent(new CustomEvent('klikklash:closeContentPanels')); window.dispatchEvent(new CustomEvent('klikklash:closeCenterAssistant')); window.dispatchEvent(new CustomEvent('klikklash:closeChatWidgets')); setShowModal(true); }}
onOpenAirNote={() => { setShowShortcuts(false); onOpenAirNote?.(); }}
onOpenContracts={() => { setShowShortcuts(false); onOpenContracts?.(); }}
onOpenAirBloom={() => { setShowShortcuts(false); onOpenAirBloom?.(); }}
onOpenBoutique={() => { setShowShortcuts(false); onOpenBoutique?.(); }}
onOpenHelp={() => { setShowShortcuts(false); setShowHelp(true); }}
/>
)}
{showModal && setShowModal(false)} personalReferralCode={personalReferralCode} />}
{showHelp && { setShowHelp(false); setShowShortcuts(true); }} initialOpenKey={null} />}
>
);
};
const WhiteDropdown = ({ value, onChange, options }) => {
const [open, setOpen] = useState(false);
const [rect, setRect] = useState(null);
const triggerRef = useRef(null);
const portalRef = useRef(null);
const selectedLabel = options.find((o) => o.value === value)?.label || options[0]?.label || '';
useEffect(() => {
const handleClickOutside = (event) => {
const isOutsideTrigger = triggerRef.current && !triggerRef.current.contains(event.target);
const isOutsidePortal = portalRef.current && !portalRef.current.contains(event.target);
if (isOutsideTrigger && isOutsidePortal) setOpen(false);
};
if (open) document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
return (
{
if (!open) {
const r = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - r.bottom;
const spaceAbove = r.top;
const openUp = spaceBelow < 240 && spaceAbove > spaceBelow;
setRect({ ...r, top: r.top, bottom: r.bottom, left: r.left, width: r.width, openUp, spaceAbove, spaceBelow });
setOpen(true);
} else setOpen(false);
}}
style={{
width: '100%', boxSizing: 'border-box', padding: 10, borderRadius: 10,
border: '1px solid rgba(255,255,255,0.15)', background: 'rgba(40,40,42,1)',
color: '#fff', fontSize: 12.5, cursor: 'pointer',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}
>
{selectedLabel}
{open && rect && createPortal(
{options.map((o) => (
{ onChange(o.value); setOpen(false); }}
style={{
padding: '9px 14px', fontSize: 13, color: '#1d1d1f', cursor: 'pointer',
background: o.value === value ? '#f2f2f7' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}
onMouseEnter={(e) => e.currentTarget.style.background = '#f2f2f7'}
onMouseLeave={(e) => e.currentTarget.style.background = o.value === value ? '#f2f2f7' : 'transparent'}
>
{o.label}
{o.value === value && }
))}
,
document.body
)}
);
};
// Correspondance raccourci → préfixes de `type` dans la table `notifications`
// (types réels trouvés dans le code, ex: airbloom_*, airnote_*, contract_*,
// nuit_des_couronnes, klik, klash, boost, support...). 'workspace_message'
// et 'affiliation_bonus' sont générés par des triggers SQL dédiés (voir
// create_workspace_and_affiliation_notifications.sql) — pas de type natif
// avant, on les a créés spécifiquement pour ces deux raccourcis.
//
// Audité le 05/09/2026 : cette liste ne couvrait ni Dream, ni AirCreate, ni
// AirShop (boutique), ni Espace Partenaire — ces 4 raccourcis n'affichaient
// donc jamais de badge, quel que soit le nombre de notifications réelles
// en attente pour ces modules. Complété en listant tous les types réels
// insérés dans `notifications` par le code (RPC + triggers SQL) trouvés à
// cette date. Si un nouveau type de notification est ajouté plus tard,
// penser à le répercuter ici (et dans NotificationsView.jsx pour son
// affichage détaillé, et dans le petit modal AirNote si besoin).
const NOTIF_TYPE_MAP = {
klik: ['klik', 'klik_comment', 'klik_reply', 'comment', 'like', 'mention', 'reply', 'reklik'],
klash: ['klash', 'challenge', 'challenge_won', 'challenge_reminder', 'challenge_notif_request', 'challenge_completed', 'task_comment', 'comment', 'like', 'mention', 'reply', 'new_klash_from_followed'],
airnote: ['airnote_invite', 'airnote_item', 'airnote_offer', 'airnote_offer_message', 'airnote_offer_status', 'airnote_besoin_resolved', 'airnote_partner_subscription_ending_soon', 'airnote_partner_deactivated', 'airnote_space', 'airnote_group_activity', 'airnote_besoin_taken', 'airdeal_purchased', 'airdeal_expired'],
dream: ['dream_new_response', 'dream_response_unlocked', 'dream_request_no_response', 'dream_unlock_ending_soon', 'partner_subscription_ending_soon', 'dream_partner_ending_soon', 'dream_request_deletion_warning', 'dream_response_deletion_warning', 'dream_client_contact', 'dream_reservation_request', 'partner_offers_deactivated'],
aircreate: ['aircreate_rating_received', 'aircreate_blue_challenge_visited', 'aircreate_blue_challenge_won', 'aircreate_blue_challenge_completed', 'aircreate_blue_challenge_created', 'aircreate_partner_subscription_ending_soon', 'aircreate_partner_deactivated'],
contracts: ['contract', 'contract_application', 'contract_listing', 'contract_message', 'contract_welcome', 'contract_proposal', 'contract_settled', 'collaboration_confirm_needed', 'collaboration_partner_confirmed', 'collaboration_confirmed', 'collaboration_report', 'collaboration_report_admin'],
airbloom: ['airbloom_boost', 'airbloom_challenge', 'airbloom_invite', 'airbloom_season', 'airbloom_season_funding', 'airbloom_task_completed', 'airbloom_task_assigned', 'airbloom_task_response', 'airbloom_challenge_reward'],
boutique: ['boutique_visit', 'boutique_article_click', 'boutique_gift_reservation', 'purchase_confirmation', 'boutique_client_contact', 'boutique_article_review', 'boutique_certified', 'boutique_boost_ended', 'team_delegation_invite', 'team_delegation_response', 'team_delegation_revoked', 'team_delegation_left'],
partner: ['partner_commission', 'referral_bonus', 'partner_challenge', 'payout_requested', 'partner_signup_reward'],
couronnes: ['nuit_des_couronnes', 'couronnes_participation', 'crown_campaign', 'crown_campaign_reply', 'crown_campaign_ending_soon'],
promo: ['service_promo_message', 'service_promo_digest', 'sponsored', 'sponsor_commission', 'sponsor_campaign_ending_soon'],
boost: ['boost', 'boost_invite', 'airbuzz_boost_started', 'airbuzz_boost_ending_soon', 'buzz_boost_reward', 'buzz', 'buzz_participation'],
support: ['support'],
notifications: null, // toutes les notifications non lues, tous types confondus
workspace: ['workspace_message', 'partner_team_joined', 'partner_team_left', 'partner_team_ownership_transferred', 'partner_team_kicked', 'team_message', 'team_task_assigned', 'team_goal_reached', 'team_invite_pending'],
affiliation: ['affiliation_bonus', 'referral_signup_reward'],
messages: ['message'],
relations: ['follow'],
};
const truncate = (s, n) => (s && s.length > n ? s.slice(0, n - 1) + '…' : s);
const formatBadgeCount = (n) => (n > 99 ? '+99' : String(n));
const ShortcutsGlassPanel = ({ onClose, onOpenPartnerSpace, onOpenWorkspace, onOpenAirNote, onOpenContracts, onOpenAirBloom, onOpenBoutique, onOpenHelp }) => {
const { user } = useAuth();
const { openAssistant, setLeftChatActiveView } = useAssistant();
const openLeftChat = (view) => { onClose?.(); setLeftChatActiveView(view); openAssistant(); };
const [activeTab, setActiveTab] = useState('shortcuts');
const [notifByKey, setNotifByKey] = useState({}); // { [key]: { unread: number, lastText: string|null } }
const [partnerUnread, setPartnerUnread] = useState(0);
const [messagesUnread, setMessagesUnread] = useState(0);
const [aircreateDemandUnread, setAircreateDemandUnread] = useState({ count: 0, lastText: null });
useEffect(() => {
if (!user?.id) return;
// Notifications par module (tous les types connus sur les 100 dernières)
supabase.from('notifications')
.select('type, title, body, is_read, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(150)
.then(({ data }) => {
if (!data) return;
const result = {};
Object.entries(NOTIF_TYPE_MAP).forEach(([key, prefixes]) => {
const matching = prefixes === null ? data : data.filter((n) => prefixes.includes(n.type));
const unread = matching.filter((n) => !n.is_read).length;
const last = matching[0];
result[key] = { unread, lastText: last ? (last.title || last.body || null) : null };
});
setNotifByKey(result);
});
// Compteur Espace Partenaire — même requête que le badge existant sur
// le bouton fantôme (ChatInputBar.jsx / ChatInput.jsx), pour rester cohérent.
supabase.from('profiles').select('partner_space_last_seen_at').eq('id', user.id).maybeSingle().then(({ data: profileData }) => {
supabase.from('affiliates').select('referral_code').eq('user_id', user.id).maybeSingle().then(({ data }) => {
if (!data?.referral_code) return;
const lastSeen = profileData?.partner_space_last_seen_at;
let signupsQuery = supabase.from('partner_conversions').select('id', { count: 'exact', head: true }).eq('referral_code', data.referral_code);
let paymentsQuery = supabase.from('partner_conversions').select('id', { count: 'exact', head: true }).eq('referral_code', data.referral_code).not('paid_at', 'is', null);
if (lastSeen) {
signupsQuery = signupsQuery.gt('created_at', lastSeen);
paymentsQuery = paymentsQuery.gt('paid_at', lastSeen);
}
Promise.all([signupsQuery, paymentsQuery]).then(([signupsRes, paymentsRes]) => {
setPartnerUnread((signupsRes.count || 0) + (paymentsRes.count || 0));
});
});
});
// Messages non lus — même requête que le badge existant du bouton fantôme.
supabase.from('messages').select('id', { count: 'exact', head: true }).eq('recipient_id', user.id).is('read_at', null)
.then(({ count }) => setMessagesUnread(count || 0));
// Notifications de demandes AirCreate — table dédiée aircreate_notifications,
// séparée de la table notifications générale (voir mémoire de session :
// "notifications carte bleue (table notifications) et notifications
// demandes (table aircreate_notifications) fusionnées dans l'affichage du
// profil"). Sans cette requête, le raccourci AirCreate ne reflétait que
// les défis carte bleue, jamais les demandes/réponses reçues.
supabase.from('aircreate_notifications')
.select('message, is_read, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(50)
.then(({ data }) => {
if (!data) return;
setAircreateDemandUnread({
count: data.filter((n) => !n.is_read).length,
lastText: data[0]?.message || null,
});
});
}, [user?.id]);
const badgeFor = (key) => {
if (key === 'partner') return partnerUnread;
if (key === 'messages') return messagesUnread;
if (key === 'aircreate') return (notifByKey.aircreate?.unread || 0) + aircreateDemandUnread.count;
return notifByKey[key]?.unread || 0;
};
const lastTextFor = (key) => {
if (key === 'aircreate') return aircreateDemandUnread.lastText || notifByKey.aircreate?.lastText || null;
return notifByKey[key]?.lastText || null;
};
const sections = [
{
title: 'Contenu',
items: [
{ key: 'klik', label: 'KLIK', emoji: '⚡', gradient: 'linear-gradient(135deg, rgba(24,140,195,0.65), rgba(133,213,235,0.45))', glow: 'rgba(24,140,195,0.3)', action: () => openLeftChat('messages') },
{ key: 'klash', label: 'KLASH', emoji: '🏆', gradient: 'linear-gradient(135deg, rgba(216,150,24,0.65), rgba(245,197,90,0.45))', glow: 'rgba(216,150,24,0.3)', action: () => openLeftChat('tasks') },
{ key: 'airnote', label: 'AirNote', emoji: '📝', gradient: 'linear-gradient(135deg, rgba(24,95,165,0.65), rgba(133,183,235,0.45))', glow: 'rgba(24,95,165,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openAirNote')); } },
{ key: 'contracts', label: 'DealTower', emoji: '🏢', gradient: 'linear-gradient(135deg, rgba(120,120,128,0.65), rgba(180,180,188,0.45))', glow: 'rgba(120,120,128,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openMyContracts')); } },
{ key: 'airbloom', label: 'AirBloom', emoji: '✨', gradient: 'linear-gradient(135deg, rgba(219,39,119,0.65), rgba(244,114,182,0.45))', glow: 'rgba(219,39,119,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openAirBloom')); } },
{ key: 'dream', label: 'Dream', emoji: '💫', gradient: 'linear-gradient(135deg, rgba(94,92,230,0.65), rgba(157,151,245,0.45))', glow: 'rgba(94,92,230,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openDream')); } },
{ key: 'aircreate', label: 'AirCreate', emoji: '📣', gradient: 'linear-gradient(135deg, rgba(216,90,48,0.65), rgba(240,153,123,0.45))', glow: 'rgba(216,90,48,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openAirCreate')); } },
{ key: 'boutique', label: 'AirShop', emoji: '🛍️', gradient: 'linear-gradient(135deg, rgba(216,150,24,0.65), rgba(245,197,90,0.45))', glow: 'rgba(216,150,24,0.3)', action: onOpenBoutique },
{ key: 'music', label: 'Ta musique', emoji: '🎵', gradient: 'linear-gradient(135deg, rgba(30,215,96,0.6), rgba(29,158,117,0.45))', glow: 'rgba(30,215,96,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openMusicPlayer')); } },
],
},
{
title: 'Partenariat',
items: [
{ key: 'partner', label: 'Espace Partenaire', emoji: '💰', gradient: 'linear-gradient(135deg, rgba(216,150,24,0.65), rgba(245,197,90,0.45))', glow: 'rgba(216,150,24,0.3)', action: onOpenPartnerSpace },
{ key: 'workspace', label: 'Espace de travail', emoji: '💬', gradient: 'linear-gradient(135deg, rgba(29,158,117,0.65), rgba(93,202,165,0.45))', glow: 'rgba(29,158,117,0.3)', action: onOpenWorkspace },
{ key: 'affiliation', label: 'Affiliation', emoji: '🔗', gradient: 'linear-gradient(135deg, rgba(24,95,165,0.65), rgba(133,183,235,0.45))', glow: 'rgba(24,95,165,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openProfileTab', { detail: { tab: 'affiliation' } })); } },
{ key: 'promo', label: 'Promo', emoji: '🎁', gradient: 'linear-gradient(135deg, rgba(216,90,48,0.65), rgba(240,153,123,0.45))', glow: 'rgba(216,90,48,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openProfileTab', { detail: { tab: 'promo1' } })); } },
{ key: 'boost', label: 'Boost', emoji: '🚀', gradient: 'linear-gradient(135deg, rgba(94,92,230,0.65), rgba(157,151,245,0.45))', glow: 'rgba(94,92,230,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openProfileTab', { detail: { tab: 'promo2' } })); } },
],
},
{
title: 'Récompenses',
items: [
{ key: 'couronnes', label: 'Nuit des Couronnes', emoji: '👑', gradient: 'linear-gradient(135deg, rgba(216,150,24,0.65), rgba(245,197,90,0.45))', glow: 'rgba(216,150,24,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openCouronnes')); } },
],
},
{
title: 'Communication',
items: [
{ key: 'newchat', label: 'Nouvelle discussion', emoji: '✉️', gradient: 'linear-gradient(135deg, rgba(24,140,195,0.65), rgba(133,213,235,0.45))', glow: 'rgba(24,140,195,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openNewChat')); } },
{ key: 'messages', label: 'Gérer les discussions', emoji: '📨', gradient: 'linear-gradient(135deg, rgba(94,92,230,0.65), rgba(157,151,245,0.45))', glow: 'rgba(94,92,230,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openMessages')); } },
{ key: 'notifications', label: 'Notifications', emoji: '🔔', gradient: 'linear-gradient(135deg, rgba(216,150,24,0.65), rgba(245,197,90,0.45))', glow: 'rgba(216,150,24,0.3)', action: () => openLeftChat('notifications') },
],
},
{
title: 'Compte',
items: [
{ key: 'profile', label: 'Profil', emoji: '👤', gradient: 'linear-gradient(135deg, rgba(29,158,117,0.65), rgba(93,202,165,0.45))', glow: 'rgba(29,158,117,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openProfileTab', { detail: { tab: 'klikklash' } })); } },
{ key: 'relations', label: 'Relations', emoji: '❤️', gradient: 'linear-gradient(135deg, rgba(219,39,119,0.65), rgba(244,114,182,0.45))', glow: 'rgba(219,39,119,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openRelations')); } },
{ key: 'settings', label: 'Réglages', emoji: '⚙️', gradient: 'linear-gradient(135deg, rgba(120,120,128,0.65), rgba(180,180,188,0.45))', glow: 'rgba(120,120,128,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openSettings')); } },
{ key: 'support', label: 'Support', emoji: '🙂', gradient: 'linear-gradient(135deg, rgba(216,60,48,0.65), rgba(240,133,123,0.45))', glow: 'rgba(216,60,48,0.3)', action: () => { onClose?.(); window.dispatchEvent(new CustomEvent('klikklash:openSupport')); } },
],
},
];
const tabs = [
{ key: 'shortcuts', icon: LayoutGrid },
{ key: 'search', icon: Search },
{ key: 'notes', icon: PenLine },
];
const tabLabels = { shortcuts: 'Raccourcis', search: 'Recherche', notes: 'Notes' };
return createPortal(
e.stopPropagation()}
style={{
width: '100%', maxWidth: 420, borderRadius: '20px 20px 0 0', boxSizing: 'border-box',
maxHeight: '92vh', overflow: 'hidden',
display: 'flex', flexDirection: 'column',
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)', boxShadow: '0 -8px 40px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.12)',
transform: 'translateZ(0)',
}}
>
{tabLabels[activeTab]}
{tabs.map((t) => (
{
if (t.key === 'search') {
window.dispatchEvent(new CustomEvent('klikklash:openSearch', { detail: { returnTo: 'tasksPanel' } }));
onClose();
} else {
setActiveTab(t.key);
}
}}
style={{
width: 34, height: 34, borderRadius: 10, cursor: 'pointer',
border: activeTab === t.key ? '1.5px solid #0a84ff' : '1px solid rgba(255,255,255,0.15)',
background: activeTab === t.key ? 'rgba(10,132,255,0.18)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
))}
{ onOpenHelp?.(); }}
aria-label="Centre d'aide"
style={{
width: 34, height: 34, borderRadius: 10, cursor: 'pointer',
border: '1px solid rgba(255,255,255,0.15)', background: 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
{activeTab === 'shortcuts' && (
{sections.map((sec) => (
{sec.title}
{sec.items.map((it) => {
const count = badgeFor(it.key);
const lastText = lastTextFor(it.key);
return (
{it.emoji}
{count > 0 && (
{formatBadgeCount(count)}
)}
{it.label}
{lastText && (
🔔 {truncate(lastText, 42)}
)}
);
})}
))}
)}
{activeTab === 'notes' &&
}
,
document.body
);
};
const NOTE_COLORS = {
white: '#ffffff',
blue: 'linear-gradient(160deg, #0a84ff, #378add)',
pink: 'linear-gradient(160deg, #ff2d95, #af52de)',
orange: 'linear-gradient(160deg, #ff9500, #ff453a)',
purple: 'linear-gradient(160deg, #7f77dd, #af52de)',
dark: 'linear-gradient(160deg, #3a3a3c, #1c1c1e)',
green: 'linear-gradient(160deg, #34c759, #1d9e75)',
};
const MODULES = [
{ key: 'dream', label: 'Dream' },
{ key: 'airnote', label: 'AirNote' },
{ key: 'aircreate', label: 'AirCreate' },
{ key: 'contrats', label: 'DealTower' },
{ key: 'airbloom', label: 'AirBloom' },
{ key: 'klik', label: 'KLIK' },
{ key: 'klash', label: 'KLASH' },
{ key: 'airmessenger', label: 'AirMessenger' },
{ key: 'couronnes', label: 'Nuit des Couronnes' },
{ key: 'airbuzz', label: 'AirBuzz' },
{ key: 'partenaire', label: 'Programme Partenaire' },
{ key: 'affiliation', label: 'Affiliation' },
{ key: 'promo', label: 'Promo' },
{ key: 'boost', label: 'Boost' },
];
const PlatformNotesTab = () => {
const { user } = useAuth();
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [filterModule, setFilterModule] = useState('');
const fetchNotes = async () => {
setLoading(true);
const { data } = await supabase
.from('platform_notes')
.select('*, profiles:author_id(kliker_name, user_name, verified_badge_active)')
.order('created_at', { ascending: false })
.limit(30);
setNotes(data || []);
setLoading(false);
};
useEffect(() => { fetchNotes(); }, []);
const filteredNotes = notes.filter((n) => {
const matchesSearch = !searchQuery.trim() || n.content.toLowerCase().includes(searchQuery.trim().toLowerCase());
const matchesFilter = !filterModule || n.linked_module === filterModule;
return matchesSearch && matchesFilter;
});
if (showCreate) {
return setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchNotes(); }} />;
}
return (
setShowCreate(true)} style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, padding: 12, borderRadius: 14, border: 'none', background: '#0a84ff', color: '#fff', fontSize: 13.5, fontWeight: 700, cursor: 'pointer', marginBottom: 10 }}>
Nouvelle note
setSearchQuery(e.target.value)} placeholder="Chercher dans les notes..." style={{ flex: 1, background: 'none', border: 'none', outline: 'none', color: '#fff', fontSize: 12.5, fontFamily: 'inherit' }} />
({ value: m.key, label: m.label }))]} />
{loading ? (
{[0, 1, 2].map((i) => (
))}
) : filteredNotes.length === 0 ? (
{notes.length === 0 ? "Aucune note pour l'instant." : 'Aucune note ne correspond.'}
) : (
{filteredNotes.map((n, i) => {
const patternIndex = i % 4;
const isWide = patternIndex === 1;
const isTall = patternIndex === 0;
const isWhite = n.color_key === 'white';
const textColor = isWhite ? '#1d1d1f' : '#fff';
const metaColor = isWhite ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.8)';
const metaColorFaint = isWhite ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.65)';
const badgeBg = isWhite ? 'rgba(0,0,0,0.06)' : 'rgba(0,0,0,0.2)';
const badgeColor = isWhite ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.85)';
return (
{n.linked_module && (
{MODULES.find((m) => m.key === n.linked_module)?.label || n.linked_module}
)}
{n.content}
{n.profiles?.kliker_name || n.profiles?.user_name}
{n.scheduled_at ? new Date(n.scheduled_at).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : new Date(n.created_at).toLocaleDateString('fr-FR')}
);
})}
)}
);
};
const CreateNoteForm = ({ onClose, onCreated }) => {
const { user } = useAuth();
const [content, setContent] = useState('');
const [scheduledAt, setScheduledAt] = useState('');
const [colorKey, setColorKey] = useState('blue');
const [linkedModule, setLinkedModule] = useState('');
const [mentionQuery, setMentionQuery] = useState('');
const [mentionResults, setMentionResults] = useState([]);
const [recentContacts, setRecentContacts] = useState([]);
const [showMentionPanel, setShowMentionPanel] = useState(false);
const [mentionTab, setMentionTab] = useState('recent');
const [myRelations, setMyRelations] = useState([]);
const [loadingRelations, setLoadingRelations] = useState(false);
const [pendingMentions, setPendingMentions] = useState([]);
const [mentionedUsers, setMentionedUsers] = useState([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (mentionQuery.trim().length < 2) { setMentionResults([]); return; }
const t = setTimeout(async () => {
const { data } = await supabase.from('profiles').select('id, kliker_name, user_name, profile_photo_url, verified_badge_active')
.or(`kliker_name.ilike.%${mentionQuery.trim()}%,user_name.ilike.%${mentionQuery.trim()}%`).limit(6);
setMentionResults(data || []);
}, 300);
return () => clearTimeout(t);
}, [mentionQuery]);
useEffect(() => {
(async () => {
const { data: msgs } = await supabase
.from('messages')
.select('sender_id, recipient_id, created_at')
.or(`sender_id.eq.${user.id},recipient_id.eq.${user.id}`)
.order('created_at', { ascending: false })
.limit(40);
const otherIds = [...new Set((msgs || []).map((m) => (m.sender_id === user.id ? m.recipient_id : m.sender_id)))].filter((id) => id !== user.id).slice(0, 8);
if (otherIds.length === 0) return;
const { data: profiles } = await supabase.from('profiles').select('id, kliker_name, user_name, profile_photo_url, verified_badge_active').in('id', otherIds);
setRecentContacts(otherIds.map((id) => profiles?.find((p) => p.id === id)).filter(Boolean));
})();
}, [user.id]);
useEffect(() => {
if (!showMentionPanel || !user?.id) return;
setLoadingRelations(true);
supabase.from('relations_likes').select('user_id').eq('target_id', user.id).then(async ({ data: fans }) => {
const ids = [...new Set((fans || []).map((f) => f.user_id))].filter((id) => id !== user.id);
if (ids.length === 0) { setMyRelations([]); setLoadingRelations(false); return; }
const { data: profs } = await supabase.from('profiles')
.select('id, kliker_name, user_name, profile_photo_url, verified_badge_active')
.in('id', ids);
setMyRelations(profs || []);
setLoadingRelations(false);
});
}, [showMentionPanel, user?.id]);
const handleCreate = async () => {
if (!content.trim()) { setError('Le texte de la note est obligatoire.'); return; }
setSaving(true);
setError('');
try {
const { error: rpcError } = await supabase.rpc('create_platform_note', {
p_content: content.trim(),
p_scheduled_at: scheduledAt ? new Date(scheduledAt).toISOString() : null,
p_color_key: colorKey,
p_linked_module: linkedModule || null,
p_mentioned_user_ids: mentionedUsers.map((u) => u.id),
});
if (rpcError) throw rpcError;
onCreated();
} catch (err) {
setError(err?.message || 'Une erreur est survenue.');
} finally {
setSaving(false);
}
};
return (
);
};