import { useState } from "react"; import { Page } from "@/components/layout/app-shell"; import { PageHeader } from "@/components/layout/page-header"; import { useQuery } from "@tanstack/react-query"; import { CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2, Clapperboard, Search, Sparkles } from "lucide-react"; import { Link } from "wouter"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { EmptyState } from "@/components/empty-state"; import { PlatformIcon } from "@/components/platform-icon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import type { ScheduledPost, SocialPage, Post, Media } from "@shared/schema"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import { PreviewModal } from "@/components/preview-modal"; import { useToast } from "@/hooks/use-toast"; type ScheduledPostWithRelations = ScheduledPost & { post?: Post; page?: SocialPage; }; /** Publication dont l'heure est passée mais que le scheduler n'a pas encore traitée. */ function isAwaitingPublication(scheduledPost: ScheduledPostWithRelations): boolean { return ( !!scheduledPost.scheduledAt && new Date(scheduledPost.scheduledAt) <= new Date() && !scheduledPost.publishedAt ); } type StatusFilter = "all" | "published" | "pending" | "failed"; const STATUS_TABS: { value: StatusFilter; label: string }[] = [ { value: "all", label: "Toutes" }, { value: "published", label: "Publiées" }, { value: "pending", label: "En cours" }, { value: "failed", label: "Échecs" }, ]; const PAGE_SIZE = 20; export default function History() { const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [previewModalOpen, setPreviewModalOpen] = useState(false); const [previewData, setPreviewData] = useState<{ postText: string; mediaIds: string[]; allMedia: Media[]; generationStatus?: string; generationProgress?: number }>({ postText: '', mediaIds: [], allMedia: [] }); const { toast } = useToast(); const { data: scheduledPosts = [], isLoading } = useQuery({ queryKey: ['/api/scheduled-posts'], // Le scheduler publie en tâche de fond : on ne suit à la seconde que ce qui // est réellement en attente. Le reste du temps, une veille lente suffit — // sans quoi la page entière était rechargée 20 fois par minute pour rien. refetchInterval: (query) => (query.state.data ?? []).some(isAwaitingPublication) ? 3000 : 30000, }); // Filter attempted posts (scheduled in the past) const now = new Date(); const publishedPosts = scheduledPosts .filter(sp => sp.scheduledAt && new Date(sp.scheduledAt) <= now) .sort((a, b) => new Date(b.scheduledAt!).getTime() - new Date(a.scheduledAt!).getTime()); const handlePreviewPost = async (scheduledPost: ScheduledPostWithRelations) => { try { const response = await fetch(`/api/posts/${scheduledPost.postId}`, { credentials: "include", }); if (!response.ok) { throw new Error("Failed to fetch post"); } const postWithMedia = await response.json(); const mediaIds = postWithMedia.media?.map((m: Media) => m.id) || []; const allMedia = postWithMedia.media || []; setPreviewData({ postText: postWithMedia.post.content || '', mediaIds: mediaIds, allMedia: allMedia, generationStatus: postWithMedia.post.generationStatus, generationProgress: postWithMedia.post.generationProgress, }); setPreviewModalOpen(true); } catch (error) { toast({ title: "Erreur", description: "Impossible de charger la prévisualisation", variant: "destructive", }); } }; const getPostTypeIcon = (postType: string) => { if (postType === 'feed') { return ; } else if (postType === 'story') { return ; } else if (postType === 'reel') { return ; } else if (postType === 'both') { return (
); } return null; }; const statusOf = (sp: ScheduledPostWithRelations): StatusFilter => sp.error ? "failed" : !sp.publishedAt ? "pending" : "published"; const counts = publishedPosts.reduce( (acc, sp) => { acc[statusOf(sp)] += 1; return acc; }, { all: publishedPosts.length, published: 0, pending: 0, failed: 0 } as Record, ); const term = searchTerm.trim().toLowerCase(); const filteredPosts = publishedPosts.filter( (sp) => (statusFilter === "all" || statusOf(sp) === statusFilter) && (!term || sp.post?.content?.toLowerCase().includes(term) || sp.page?.pageName?.toLowerCase().includes(term)), ); const visiblePosts = filteredPosts.slice(0, visibleCount); return ( <>
{ setStatusFilter(v as StatusFilter); setVisibleCount(PAGE_SIZE); }}> {STATUS_TABS.map((t) => ( {t.label} {counts[t.value]} ))}
{ setSearchTerm(e.target.value); setVisibleCount(PAGE_SIZE); }} className="pl-9" data-testid="input-history-search" />
{isLoading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : publishedPosts.length === 0 ? ( Créer une publication } /> ) : filteredPosts.length === 0 ? ( ) : (
{visiblePosts.map((scheduledPost) => { const status = statusOf(scheduledPost); return (

{scheduledPost.page?.pageName || "Page inconnue"}

{status === "failed" ? ( Échec ) : status === "pending" ? ( En cours ) : ( Publié )} {scheduledPost.post?.aiGenerated && ( IA )}

{scheduledPost.post?.content || Aucun texte}

{getPostTypeIcon(scheduledPost.postType)} {scheduledPost.postType === "feed" ? "Feed" : scheduledPost.postType === "story" ? "Story" : scheduledPost.postType === "reel" ? "Reel" : "Feed & Story"} {scheduledPost.scheduledAt ? format(new Date(scheduledPost.scheduledAt), "d MMM yyyy 'à' HH:mm", { locale: fr }) : "Date inconnue"}
{scheduledPost.error && (

{scheduledPost.error}

)} {scheduledPost.error && !scheduledPost.publishedAt && (

{scheduledPost.nextAttemptAt ? `Nouvelle tentative automatique le ${format(new Date(scheduledPost.nextAttemptAt), "d MMM yyyy 'à' HH:mm", { locale: fr })}` : scheduledPost.attempts > 0 ? `Abandon après ${scheduledPost.attempts} tentatives : reprogrammez la publication pour réessayer.` : null}

)}
); })} {filteredPosts.length > visibleCount && (
)}
)} { }} isPublishing={false} readOnly={true} generationStatus={previewData.generationStatus} generationProgress={previewData.generationProgress} /> ); }