diff --git a/client/src/components/calendar-list-view.tsx b/client/src/components/calendar-list-view.tsx index 82105eb..0ca4a3e 100644 --- a/client/src/components/calendar-list-view.tsx +++ b/client/src/components/calendar-list-view.tsx @@ -1,6 +1,6 @@ import { format, isSameDay, parseISO } from "date-fns"; import { fr } from "date-fns/locale"; -import { Edit, Trash2, ChevronDown, ChevronUp, Eye } from "lucide-react"; +import { Edit, Trash2, ChevronDown, ChevronUp, Eye, Image, Smartphone } from "lucide-react"; import { SiFacebook, SiInstagram } from "react-icons/si"; import { Button } from "@/components/ui/button"; import { useState } from "react"; @@ -15,6 +15,22 @@ interface CalendarListViewProps { export default function CalendarListView({ scheduledPosts, onEditPost, onDeletePost, onPreviewPost }: CalendarListViewProps) { const [expandedDates, setExpandedDates] = useState>(new Set()); + const getPostTypeIcon = (postType: string) => { + if (postType === 'feed') { + return ; + } else if (postType === 'story') { + return ; + } else if (postType === 'both') { + return ( +
+ + +
+ ); + } + return null; + }; + // Group posts by date const groupedPosts = scheduledPosts.reduce((acc: { [key: string]: any[] }, post) => { if (!post.scheduledAt) return acc; @@ -115,6 +131,7 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP
+ {getPostTypeIcon(post.postType)} {time} ({ queryKey: ["/api/scheduled-posts"], + refetchInterval: 30000, // Auto-refresh every 30 seconds }); const deletePostMutation = useMutation({ @@ -90,6 +91,22 @@ export default function CalendarView() { } }; + const getPostTypeIcon = (postType: string) => { + if (postType === 'feed') { + return ; + } else if (postType === 'story') { + return ; + } else if (postType === 'both') { + return ( +
+ + +
+ ); + } + return null; + }; + const getPostsForDate = (date: Date) => { return (scheduledPosts || []).filter(post => { if (!post.scheduledAt) return false; @@ -256,7 +273,10 @@ export default function CalendarView() { >
-
{time}
+
+ {getPostTypeIcon(post.postType)} + {time} +
{pageName} @@ -332,7 +352,10 @@ export default function CalendarView() { >
-
{time}
+
+ {getPostTypeIcon(post.postType)} + {time} +
{pageName} diff --git a/client/src/components/recent-publications.tsx b/client/src/components/recent-publications.tsx index 550d0ab..30362be 100644 --- a/client/src/components/recent-publications.tsx +++ b/client/src/components/recent-publications.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Facebook, Instagram, Clock, CheckCircle2, XCircle, Eye } from "lucide-react"; +import { Facebook, Instagram, Clock, CheckCircle2, XCircle, Eye, Image as ImageIcon, Smartphone } from "lucide-react"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import type { ScheduledPost, SocialPage, Post, Media } from "@shared/schema"; @@ -60,6 +60,22 @@ export default function RecentPublications() { } }; + const getPostTypeIcon = (postType: string) => { + if (postType === 'feed') { + return ; + } else if (postType === 'story') { + return ; + } else if (postType === 'both') { + return ( +
+ + +
+ ); + } + return null; + }; + return ( @@ -136,9 +152,10 @@ export default function RecentPublications() {
- - {scheduledPost.postType === 'feed' ? 'Publication' : - scheduledPost.postType === 'story' ? 'Story' : scheduledPost.postType} + + {getPostTypeIcon(scheduledPost.postType)} + {scheduledPost.postType === 'feed' ? 'Feed' : + scheduledPost.postType === 'story' ? 'Story' : 'Feed & Story'} • diff --git a/client/src/pages/history.tsx b/client/src/pages/history.tsx index 37154a2..3b770fb 100644 --- a/client/src/pages/history.tsx +++ b/client/src/pages/history.tsx @@ -1,22 +1,83 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { Clock, CheckCircle, XCircle, Calendar, History as HistoryIcon } from "lucide-react"; +import { Clock, CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone } from "lucide-react"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import type { Post } from "@shared/schema"; +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; +}; export default function History() { const [sidebarOpen, setSidebarOpen] = useState(false); + const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [previewData, setPreviewData] = useState<{ postText: string; mediaIds: string[]; allMedia: Media[] }>({ postText: '', mediaIds: [], allMedia: [] }); + const { toast } = useToast(); - const { data: posts = [], isLoading } = useQuery({ - queryKey: ['/api/posts'], + const { data: scheduledPosts = [], isLoading } = useQuery({ + queryKey: ['/api/scheduled-posts'], }); - const publishedPosts = posts.filter(p => p.status === 'published'); + // 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, + }); + + 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 === 'both') { + return ( +
+ + +
+ ); + } + return null; + }; return (
@@ -65,60 +126,110 @@ export default function History() { ) : (
- {publishedPosts.map((post) => ( - - -
-
-
-
- + {publishedPosts.map((scheduledPost) => { + const isPending = !scheduledPost.publishedAt; + const hasError = !!scheduledPost.error; + + return ( + + +
+
+
+
+ {hasError ? ( + + ) : ( + + )} +
+ + {scheduledPost.page?.pageName || 'Page inconnue'} +
- - Publication #{post.id.substring(0, 8)} - +

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

+
+
+ + + {hasError ? ( + <> + + Échoué + + ) : ( + <> + + Publié + + )} +
-

- {post.content} -

- - {post.status === 'published' && } - {post.status === 'failed' && } - {post.status === 'published' ? 'Publié' : 'Échoué'} - -
- - -
-
- - - {post.updatedAt - ? format(new Date(post.updatedAt), "d MMM yyyy 'à' HH:mm", { locale: fr }) - : 'Date inconnue' - } - + + +
+
+ {getPostTypeIcon(scheduledPost.postType)} + + {scheduledPost.postType === 'feed' ? 'Feed' : + scheduledPost.postType === 'story' ? 'Story' : 'Feed & Story'} + +
+
+ + + {scheduledPost.scheduledAt + ? format(new Date(scheduledPost.scheduledAt), "d MMM yyyy 'à' HH:mm", { locale: fr }) + : 'Date inconnue' + } + +
+ {scheduledPost.post?.aiGenerated && ( + + + Généré par IA + + )}
- {post.aiGenerated && ( - - - Généré par IA - - )} -
- - - ))} + + + ); + })}
)}
+ + {}} + isPublishing={false} + readOnly={true} + />
); diff --git a/replit.md b/replit.md index 94b0244..37b5527 100644 --- a/replit.md +++ b/replit.md @@ -11,6 +11,9 @@ Preferred communication style: Simple, everyday language. ## Recent Changes ### October 10, 2025 +- **Post Type Visual Indicators**: Added visual icons throughout the app to distinguish post types at a glance. Feed posts display Image icon (🖼️), Story posts display Smartphone icon (📱), and posts configured for both show both icons side-by-side. Icons appear consistently across calendar view (desktop grid and mobile list), dashboard "Historique des publications" section, and history page for improved visual clarity. +- **Calendar Auto-Refresh**: Implemented automatic calendar data refresh every 30 seconds using TanStack Query's `refetchInterval` option. Calendar continuously fetches updated scheduled posts without requiring page reload or manual refresh, ensuring users see the latest schedule data in real-time. +- **History Preview Access Control**: Fixed GET /api/posts/:id permissions to use page-based access instead of ownership check. Users can now preview any post from their assigned pages in the history section, not just their own posts. Admin users retain full preview access to all posts. - **Calendar Permissions System**: Implemented comprehensive page-based permissions for calendar access. Admin users see all scheduled posts with full edit/delete rights. Standard users see only posts from their assigned pages via `user_page_permissions` table, but can only edit/delete their own posts (ownership check via `post.userId`). GET /api/scheduled-posts filters posts by accessible pages (admin bypass). DELETE and PATCH /api/scheduled-posts enforce ownership validation (admin can modify any post, users limited to own posts). - **Scheduled Posts Data Sync Fix**: Fixed critical bug where editing scheduled post time via PATCH /api/scheduled-posts/:id updated `scheduled_posts.scheduledAt` but not `posts.scheduledFor`, causing calendar/database inconsistency. Now both tables are synchronized: updating scheduledAt automatically updates scheduledFor via `storage.updatePost()`. - **Scheduled Posts Validation**: Added validation to POST /api/posts requiring `pageIds` when `scheduledFor` is provided. Prevents orphaned scheduled posts that would be invisible in calendar. Returns 400 error: "Les posts programmés nécessitent au moins une page cible".