import { useEffect, useState } from "react"; import { Page } from "@/components/layout/app-shell"; import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Plus, Trash2, RefreshCw, Bug, Code, ChevronDown, HelpCircle, KeyRound, Link2, MoreVertical } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { EmptyState } from "@/components/empty-state"; import { PlatformIcon, platformLabel } from "@/components/platform-icon"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { ClientSocialPage } from "@shared/schema"; import { useConnectFacebook, RefreshTokenButton, TokenAlertBanner, TokenHealthPanel, } from "@/components/facebook-token-status"; export default function PagesManagement() { const [dialogOpen, setDialogOpen] = useState(false); const [editingPage, setEditingPage] = useState(null); const [pageToDelete, setPageToDelete] = useState(null); const { toast } = useToast(); // Retour des flux d'autorisation (/api/tiktok/callback et /api/facebook/callback // redirigent ici avec le résultat en paramètre) useEffect(() => { const params = new URLSearchParams(window.location.search); const network = params.get('tiktok') ? 'tiktok' : params.get('facebook') ? 'facebook' : null; if (!network) return; const result = params.get(network); const label = network === 'tiktok' ? 'TikTok' : 'Facebook'; toast({ title: result === 'success' ? `${label} connecté` : `Connexion ${label} impossible`, description: params.get('message') || undefined, variant: result === 'success' ? undefined : 'destructive', }); queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); window.history.replaceState({}, '', window.location.pathname); }, [toast]); const { data: pages = [], isLoading } = useQuery({ queryKey: ['/api/pages'], }); const deleteMutation = useMutation({ mutationFn: (pageId: string) => apiRequest('DELETE', `/api/pages/${pageId}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); toast({ title: "Compte déconnecté", description: "Le compte a été retiré de Social Flow", }); }, onError: () => { toast({ title: "Erreur", description: "Impossible de supprimer la page", variant: "destructive", }); }, }); const connectFacebook = useConnectFacebook(); const connectTiktok = useConnectTiktok(); return (

Facebook & Instagram

Recommandé · connexion permanente

TikTok

Autorisation via TikTok

setDialogOpen(true)} className="items-start gap-3 py-2.5" data-testid="button-add-page">

Ajout manuel

Avec un jeton d'accès (expire après 60 jours)

} /> !open && setEditingPage(null)} /> {isLoading ? (
{[1, 2].map((i) => (
))}
) : pages.length === 0 ? (
} /> ) : (
{pages.map((page) => (
{page.pageName} {platformLabel(page.platform)} · {page.pageId}
{page.platform === "tiktok" ? ( { window.location.href = "/api/tiktok/connect"; }} data-testid={`button-reconnect-page-${page.id}`}> Reconnecter ) : ( <> setEditingPage(page)} data-testid={`button-edit-page-${page.id}`}> Remplacer le jeton window.open(`https://developers.facebook.com/tools/debug/accesstoken/`, "_blank")} data-testid="button-debug-token"> Déboguer le jeton )} setPageToDelete(page)} className="text-destructive focus:text-destructive" data-testid={`button-delete-page-${page.id}`} > Déconnecter
{/* État du jeton, tel que constaté par le dernier contrôle serveur */} {page.platform !== "tiktok" && (
)}
))}
)} Besoin d'aide pour connecter un compte ?

Préférez « Facebook & Instagram » plutôt que l'ajout manuel : les jetons obtenus ainsi n'expirent pas et Social Flow les régénère seul en cas de révocation. Un jeton collé à la main expire au bout de 60 jours et devra être remplacé.

Permissions nécessaires : pages_manage_posts{" "} pages_read_engagement{" "} publish_video

!open && setPageToDelete(null)}> Déconnecter « {pageToDelete?.pageName} » ? Social Flow ne pourra plus publier sur ce compte. Vous pourrez le reconnecter à tout moment. Annuler { if (pageToDelete) deleteMutation.mutate(pageToDelete.id); setPageToDelete(null); }} > Déconnecter
); } /** * Lance l'autorisation TikTok. La connexion se fait par navigation complète du * navigateur (et non en fetch) puisqu'elle passe par le site de TikTok. */ function useConnectTiktok() { const { toast } = useToast(); const { data: config } = useQuery<{ configured: boolean }>({ queryKey: ['/api/tiktok/config'], }); const handleClick = () => { if (config && !config.configured) { toast({ title: "TikTok n'est pas configuré", description: "Renseignez le client key et le client secret de votre application TikTok dans les paramètres.", variant: "destructive", }); return; } window.location.href = '/api/tiktok/connect'; }; return handleClick; } function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { const [platform, setPlatform] = useState<'facebook' | 'instagram'>('facebook'); const [pageName, setPageName] = useState(''); const [pageId, setPageId] = useState(''); const [accessToken, setAccessToken] = useState(''); const { toast } = useToast(); const addMutation = useMutation({ mutationFn: (data: any) => apiRequest('POST', '/api/pages', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); toast({ title: "Page ajoutée", description: "La page a été connectée avec succès", }); onOpenChange(false); setPlatform('facebook'); setPageName(''); setPageId(''); setAccessToken(''); }, onError: () => { toast({ title: "Erreur", description: "Impossible d'ajouter la page", variant: "destructive", }); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); addMutation.mutate({ platform, pageName, pageId, accessToken, }); }; return ( Ajouter un compte manuellement Renseignez l'identifiant de la page et un jeton d'accès. Ce jeton expirera au bout de 60 jours.
setPageName(e.target.value)} placeholder="Mon entreprise" required data-testid="input-page-name" />
setPageId(e.target.value)} placeholder="123456789" required data-testid="input-page-id" />
setAccessToken(e.target.value)} placeholder="EAAxxxxxxxxxxxxx" required data-testid="input-access-token" />

Obtenez votre jeton d'accès depuis{' '} Facebook Graph API Explorer

); } function EditPageDialog({ page, onOpenChange }: { page: ClientSocialPage | null; onOpenChange: (open: boolean) => void }) { const [accessToken, setAccessToken] = useState(''); const { toast } = useToast(); const editMutation = useMutation({ mutationFn: (data: any) => apiRequest('PUT', `/api/pages/${page?.id}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); toast({ title: "Page modifiée", description: "Le jeton d'accès a été mis à jour avec succès", }); onOpenChange(false); setAccessToken(''); }, onError: () => { toast({ title: "Erreur", description: "Impossible de modifier la page", variant: "destructive", }); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!page) return; editMutation.mutate({ accessToken, }); }; if (!page) return null; return ( Modifier le jeton d'accès Mettez à jour le jeton d'accès pour {page.pageName}
setAccessToken(e.target.value)} placeholder="EAAxxxxxxxxxxxxx" required data-testid="input-edit-access-token" />

Obtenez un nouveau jeton depuis{' '} Facebook Graph API Explorer

); }