mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-19 05:07:08 +02:00
Afficher les publications déjà prévues par jour et par magasin
Le sélecteur de date des Reels ne montrait rien de ce qui était déjà programmé, et celui des posts ne marquait qu'un jour « occupé » sans dire pour quel magasin ni à quelle heure. - GET /api/scheduled-posts/agenda : publications planifiées et Reels encore en montage (qui n'ont pas de scheduled_posts avant la fin du rendu), limités aux magasins accessibles. - Calendrier : pastille par jour, en rouge si un magasin coché a déjà une publication, en gris s'il s'agit d'autres magasins. - Heures : badge sur les heures déjà prises sur les magasins cochés. - Panneau « Déjà prévu ce jour-là » groupé par magasin, avec une alerte non bloquante en cas de doublon. - Branché sur Nouveau post, Reel et Programmer un Reel. - Icône Reel dans la vue calendrier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMU8FG2v2EE54YWHhqehPw
This commit is contained in:
11 files changed
+553
-43
No files matched your search
@@ -22,6 +22,7 @@ import { facebookRouter } from "./routes/facebook";
|
||||
import { legalRouter } from "./routes/legal";
|
||||
import { createVideoThumbnail } from "./services/thumbnail";
|
||||
import { insertAudioTrackSchema } from "@shared/schema";
|
||||
import { agendaItemFromScheduledPost, agendaItemsFromReelJob, type AgendaItem, type AgendaPage } from "@shared/agenda";
|
||||
import * as musicMetadata from "music-metadata";
|
||||
|
||||
// Types MIME autorisés pour les uploads
|
||||
@@ -1517,6 +1518,47 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Agenda : ce qui est déjà prévu par jour et par magasin, Reels en montage compris.
|
||||
// Sous /api/scheduled-posts pour être rafraîchi avec les publications planifiées.
|
||||
app.get("/api/scheduled-posts/agenda", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const start = parseDateParam(req.query.startDate);
|
||||
const end = parseDateParam(req.query.endDate);
|
||||
|
||||
const pages = user.role === 'admin'
|
||||
? await storage.getAllSocialPages()
|
||||
: await storage.getUserAccessiblePages(user.id);
|
||||
if (pages.length === 0) return res.json([]);
|
||||
|
||||
const pagesById = new Map<string, AgendaPage>(pages.map(p => [p.id, {
|
||||
id: p.id,
|
||||
pageName: p.pageName,
|
||||
platform: p.platform,
|
||||
avatarUrl: p.avatarUrl ?? null,
|
||||
}]));
|
||||
|
||||
const [scheduled, renderingJobs] = await Promise.all([
|
||||
storage.getScheduledPostsByPages(Array.from(pagesById.keys()), start, end),
|
||||
storage.getRenderingReelJobs(),
|
||||
]);
|
||||
|
||||
const items: AgendaItem[] = scheduled.map(agendaItemFromScheduledPost);
|
||||
// Un rendu qui vient de finir peut avoir déjà créé ses publications.
|
||||
const plannedPosts = new Set(items.map(i => i.postId));
|
||||
for (const job of renderingJobs) {
|
||||
if (job.postId && plannedPosts.has(job.postId)) continue;
|
||||
items.push(...agendaItemsFromReelJob(job, pagesById, start, end));
|
||||
}
|
||||
|
||||
items.sort((a, b) => a.scheduledAt.localeCompare(b.scheduledAt));
|
||||
res.json(items);
|
||||
} catch (error) {
|
||||
console.error("Error fetching publication agenda:", error);
|
||||
res.status(500).json({ error: "Failed to fetch publication agenda" });
|
||||
}
|
||||
});
|
||||
|
||||
// Scheduled posts
|
||||
app.get("/api/scheduled-posts", requireAuth, async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -149,6 +149,7 @@ export interface IStorage {
|
||||
getPost(id: string): Promise<Post | undefined>;
|
||||
getPostWithMedia(id: string): Promise<{ post: Post; media: Media[] } | undefined>;
|
||||
getOngoingReelPosts(userId: string): Promise<(Post & { generationStep: string | null })[]>;
|
||||
getRenderingReelJobs(): Promise<{ id: string; postId: string | null; createdAt: Date; params: unknown; content: string | null }[]>;
|
||||
createPost(post: InsertPost): Promise<Post>;
|
||||
updatePost(id: string, post: Partial<InsertPost>): Promise<Post>;
|
||||
updatePostGenerationStatus(id: string, status: string, progress: number, error?: string): Promise<Post>;
|
||||
@@ -582,6 +583,24 @@ export class DatabaseStorage implements IStorage {
|
||||
return rows.map((post) => ({ ...post, generationStep: stepByPost.get(post.id) ?? null }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reels vidéo en file ou en montage : leurs publications planifiées
|
||||
* n'existent qu'à la fin du rendu, l'agenda les lit donc ici.
|
||||
*/
|
||||
async getRenderingReelJobs(): Promise<{ id: string; postId: string | null; createdAt: Date; params: unknown; content: string | null }[]> {
|
||||
return await db
|
||||
.select({
|
||||
id: reelJobs.id,
|
||||
postId: reelJobs.postId,
|
||||
createdAt: reelJobs.createdAt,
|
||||
params: reelJobs.params,
|
||||
content: posts.content,
|
||||
})
|
||||
.from(reelJobs)
|
||||
.leftJoin(posts, eq(reelJobs.postId, posts.id))
|
||||
.where(and(eq(reelJobs.kind, 'video'), inArray(reelJobs.status, ['pending', 'processing'])));
|
||||
}
|
||||
|
||||
// AI Generations
|
||||
async getAiGenerations(userId: string): Promise<AiGeneration[]> {
|
||||
return await db.select().from(aiGenerations).where(eq(aiGenerations.userId, userId)).orderBy(desc(aiGenerations.createdAt));
|
||||
|
||||
Reference in new issue
Block a user