diff --git a/.replit b/.replit index d51ca2b..a942ce2 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,14 @@ externalPort = 3003 localPort = 35357 externalPort = 3002 +[[ports]] +localPort = 35385 +externalPort = 6800 + +[[ports]] +localPort = 35573 +externalPort = 8000 + [[ports]] localPort = 37431 externalPort = 5000 diff --git a/client/src/components/calendar-list-view.tsx b/client/src/components/calendar-list-view.tsx new file mode 100644 index 0000000..edac5de --- /dev/null +++ b/client/src/components/calendar-list-view.tsx @@ -0,0 +1,168 @@ +import { format, isSameDay, parseISO } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Edit, Trash2, ChevronDown, ChevronUp } from "lucide-react"; +import { SiFacebook, SiInstagram } from "react-icons/si"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; + +interface CalendarListViewProps { + scheduledPosts: any[]; + onEditPost: (post: any) => void; + onDeletePost: (postId: string) => void; +} + +export default function CalendarListView({ scheduledPosts, onEditPost, onDeletePost }: CalendarListViewProps) { + const [expandedDates, setExpandedDates] = useState>(new Set()); + + // Group posts by date + const groupedPosts = scheduledPosts.reduce((acc: { [key: string]: any[] }, post) => { + if (!post.scheduledAt) return acc; + const date = format(parseISO(post.scheduledAt), "yyyy-MM-dd"); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(post); + return acc; + }, {}); + + // Sort dates + const sortedDates = Object.keys(groupedPosts).sort(); + + const toggleDate = (date: string) => { + const newExpanded = new Set(expandedDates); + if (newExpanded.has(date)) { + newExpanded.delete(date); + } else { + newExpanded.add(date); + } + setExpandedDates(newExpanded); + }; + + const isToday = (dateStr: string) => { + return isSameDay(parseISO(dateStr), new Date()); + }; + + return ( +
+ {sortedDates.length === 0 ? ( +
+ Aucune publication programmée +
+ ) : ( + sortedDates.map((dateStr) => { + const posts = groupedPosts[dateStr]; + const isExpanded = expandedDates.has(dateStr); + const dateObj = parseISO(dateStr); + const isDateToday = isToday(dateStr); + + return ( +
+ + + {isExpanded && ( +
+ {posts.map((post: any) => { + const time = post.scheduledAt + ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + const isPending = !post.publishedAt; + const isPublished = !!post.publishedAt; + const pageName = post.page?.pageName || 'Page inconnue'; + const platform = post.page?.platform || 'facebook'; + const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook; + + return ( +
+
+
+
+ {time} + + {isPending ? 'Programmé' : 'Publié'} + +
+
+ + {pageName} +
+
+ + {isPending && ( +
+ + +
+ )} +
+
+ ); + })} +
+ )} +
+ ); + }) + )} +
+ ); +} diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 6a40ccc..1292da5 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -1,14 +1,21 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2 } from "lucide-react"; +import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2, Edit } from "lucide-react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { ScheduledPost } from "@shared/schema"; +import { SiFacebook, SiInstagram } from "react-icons/si"; +import EditScheduledPostDialog from "./edit-scheduled-post-dialog"; +import CalendarListView from "./calendar-list-view"; +import { useMediaQuery } from "@/hooks/use-media-query"; export default function CalendarView() { const [currentDate, setCurrentDate] = useState(new Date()); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [selectedPost, setSelectedPost] = useState(null); const { toast } = useToast(); + const isDesktop = useMediaQuery("(min-width: 768px)"); const { data: scheduledPosts = [] } = useQuery({ queryKey: ["/api/scheduled-posts"], @@ -33,13 +40,19 @@ export default function CalendarView() { }, }); - const handleDeletePost = (postId: string, e: React.MouseEvent) => { - e.stopPropagation(); + const handleDeletePost = (postId: string, e?: React.MouseEvent) => { + if (e) e.stopPropagation(); if (confirm("Êtes-vous sûr de vouloir supprimer cette publication programmée ?")) { deletePostMutation.mutate(postId); } }; + const handleEditPost = (post: any, e?: React.MouseEvent) => { + if (e) e.stopPropagation(); + setSelectedPost(post); + setEditDialogOpen(true); + }; + const getPostsForDate = (date: Date) => { return (scheduledPosts || []).filter(post => { if (!post.scheduledAt) return false; @@ -137,16 +150,17 @@ export default function CalendarView() { -
-
- {daysOfWeek.map((day) => ( -
- {day} -
- ))} -
+ {isDesktop ? ( +
+
+ {daysOfWeek.map((day) => ( +
+ {day} +
+ ))} +
-
+
{days.map((day, index) => (
- {postsForDay.slice(0, 2).map((post, idx) => { + {postsForDay.slice(0, 2).map((post: any, idx) => { const time = post.scheduledAt ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : ''; const isPending = !post.publishedAt; const isPublished = !!post.publishedAt; + const pageName = post.page?.pageName || 'Page inconnue'; + const platform = post.page?.platform || 'facebook'; + const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook; return (
{time}
-
{post.postType || 'Post'}
+
+ + {pageName} +
{isPending && ( - +
+ + +
)}
@@ -227,17 +257,30 @@ export default function CalendarView() { ))}
-
-
-
- Programmé -
-
-
- Publié +
+
+
+ Programmé +
+
+
+ Publié +
-
+ ) : ( + + )} + +
); } diff --git a/client/src/components/edit-scheduled-post-dialog.tsx b/client/src/components/edit-scheduled-post-dialog.tsx new file mode 100644 index 0000000..f70732d --- /dev/null +++ b/client/src/components/edit-scheduled-post-dialog.tsx @@ -0,0 +1,281 @@ +import { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import type { SocialPage, Media, Post } from "@shared/schema"; +import { format } from "date-fns"; +import { Image as ImageIcon, CheckCircle } from "lucide-react"; + +interface EditScheduledPostDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + scheduledPost: any; +} + +export default function EditScheduledPostDialog({ open, onOpenChange, scheduledPost }: EditScheduledPostDialogProps) { + const { toast } = useToast(); + const [date, setDate] = useState(""); + const [time, setTime] = useState(""); + const [pageId, setPageId] = useState(""); + const [postText, setPostText] = useState(""); + const [selectedMediaIds, setSelectedMediaIds] = useState([]); + + const { data: pages = [] } = useQuery({ + queryKey: ["/api/pages"], + }); + + const { data: postData } = useQuery<{ post: Post; media: Media[] }>({ + queryKey: ["/api/posts", scheduledPost?.postId], + enabled: !!scheduledPost?.postId && open, + }); + + const { data: allMedia = [] } = useQuery({ + queryKey: ["/api/media"], + enabled: open, + }); + + useEffect(() => { + if (scheduledPost && open) { + const scheduledAt = new Date(scheduledPost.scheduledAt); + setDate(format(scheduledAt, "yyyy-MM-dd")); + setTime(format(scheduledAt, "HH:mm")); + setPageId(scheduledPost.pageId); + } + }, [scheduledPost, open]); + + useEffect(() => { + if (postData && open) { + setPostText(postData.post.content); + setSelectedMediaIds(postData.media.map(m => m.id)); + } + }, [postData, open]); + + const updateScheduledMutation = useMutation({ + mutationFn: (data: { scheduledAt: string; pageId: string }) => + apiRequest('PATCH', `/api/scheduled-posts/${scheduledPost.id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'] }); + }, + }); + + const updatePostTextMutation = useMutation({ + mutationFn: (data: { content: string }) => + apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] }); + }, + }); + + const updatePostMediaMutation = useMutation({ + mutationFn: (data: { mediaIds: string[] }) => + apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}/media`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] }); + }, + }); + + const handleSave = async () => { + if (!date || !time || !pageId) { + toast({ + title: "Erreur", + description: "Veuillez remplir tous les champs", + variant: "destructive", + }); + return; + } + + try { + const scheduledAt = new Date(`${date}T${time}`); + + await updateScheduledMutation.mutateAsync({ scheduledAt: scheduledAt.toISOString(), pageId }); + + if (postData && postText !== postData.post.content) { + await updatePostTextMutation.mutateAsync({ content: postText }); + } + + const currentMediaIds = postData?.media.map(m => m.id) || []; + if (JSON.stringify(selectedMediaIds) !== JSON.stringify(currentMediaIds)) { + await updatePostMediaMutation.mutateAsync({ mediaIds: selectedMediaIds }); + } + + toast({ + title: "Modifications enregistrées", + description: "Le post a été mis à jour avec succès", + }); + onOpenChange(false); + } catch (error) { + toast({ + title: "Erreur", + description: "Impossible de modifier le post", + variant: "destructive", + }); + } + }; + + const toggleMediaSelection = (mediaId: string) => { + setSelectedMediaIds(prev => { + if (prev.includes(mediaId)) { + return prev.filter(id => id !== mediaId); + } else { + if (prev.length >= 10) { + toast({ + title: "Limite atteinte", + description: "Maximum 10 photos par publication", + variant: "destructive", + }); + return prev; + } + return [...prev, mediaId]; + } + }); + }; + + const isPending = updateScheduledMutation.isPending || updatePostTextMutation.isPending || updatePostMediaMutation.isPending; + + return ( + + + + Modifier le post + + +
+
+ +