Checkpoint: Ajout de la page de gestion des flux RSS : liste des flux, formulaire d'ajout/édition avec type (Veille/AAP), type par défaut, règles d'automatisme par mots-clés, paramètres de fréquence (heure fixe ou intervalle), activation/désactivation. Tables BDD rss_feeds et rss_settings. Procédures tRPC complètes. Navigation sidebar mise à jour.
This commit is contained in:
73
server/db.ts
73
server/db.ts
@@ -11,6 +11,12 @@ import {
|
||||
InsertLocalUser,
|
||||
ideas,
|
||||
InsertIdea,
|
||||
rssFeeds,
|
||||
rssSettings,
|
||||
type InsertRssFeed,
|
||||
type InsertRssSettings,
|
||||
type RssFeed,
|
||||
type RssSettings,
|
||||
} from "../drizzle/schema";
|
||||
import { ENV } from "./_core/env";
|
||||
|
||||
@@ -360,3 +366,70 @@ export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours"
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(ideas).set({ statut }).where(eq(ideas.id, id));
|
||||
}
|
||||
|
||||
// ─── Flux RSS ────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getRssFeeds(): Promise<RssFeed[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db.select().from(rssFeeds).orderBy(rssFeeds.name);
|
||||
}
|
||||
|
||||
export async function getRssFeedById(id: number): Promise<RssFeed | null> {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const rows = await db.select().from(rssFeeds).where(eq(rssFeeds.id, id)).limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function createRssFeed(data: Omit<InsertRssFeed, "id" | "createdAt" | "updatedAt">): Promise<number> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.insert(rssFeeds).values(data);
|
||||
return (result[0] as any).insertId as number;
|
||||
}
|
||||
|
||||
export async function updateRssFeed(id: number, data: Partial<Omit<InsertRssFeed, "id" | "createdAt" | "updatedAt">>): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.update(rssFeeds).set(data).where(eq(rssFeeds.id, id));
|
||||
}
|
||||
|
||||
export async function deleteRssFeed(id: number): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db.delete(rssFeeds).where(eq(rssFeeds.id, id));
|
||||
}
|
||||
|
||||
export async function getRssSettings(): Promise<RssSettings | null> {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
const rows = await db.select().from(rssSettings).limit(1);
|
||||
if (rows.length > 0) return rows[0];
|
||||
// Créer les paramètres par défaut si inexistants
|
||||
await db.insert(rssSettings).values({
|
||||
fetchIntervalMinutes: 360,
|
||||
scheduledTime: "06:00",
|
||||
fetchMode: "scheduled",
|
||||
autoFetchEnabled: true,
|
||||
});
|
||||
const newRows = await db.select().from(rssSettings).limit(1);
|
||||
return newRows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function saveRssSettings(data: Partial<Omit<InsertRssSettings, "id" | "updatedAt">>): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const existing = await db.select().from(rssSettings).limit(1);
|
||||
if (existing.length > 0) {
|
||||
await db.update(rssSettings).set(data).where(eq(rssSettings.id, existing[0].id));
|
||||
} else {
|
||||
await db.insert(rssSettings).values({
|
||||
fetchIntervalMinutes: 360,
|
||||
scheduledTime: "06:00",
|
||||
fetchMode: "scheduled",
|
||||
autoFetchEnabled: true,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@ import {
|
||||
getIdeasByUser,
|
||||
repondreIdea,
|
||||
updateIdeaStatut,
|
||||
getRssFeeds,
|
||||
getRssFeedById,
|
||||
createRssFeed,
|
||||
updateRssFeed,
|
||||
deleteRssFeed,
|
||||
getRssSettings,
|
||||
saveRssSettings,
|
||||
} from "./db";
|
||||
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
|
||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||
@@ -307,6 +314,94 @@ export const appRouter = router({
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
});
|
||||
// ─── Flux RSS ───────────────────────────────────────────────────────────────────────────────────
|
||||
rss: router({
|
||||
// Lister tous les flux
|
||||
list: protectedProcedure.query(async () => {
|
||||
return getRssFeeds();
|
||||
}),
|
||||
|
||||
// Créer un flux
|
||||
create: adminProcedure
|
||||
.input(z.object({
|
||||
url: z.string().url("URL invalide"),
|
||||
name: z.string().min(1, "Nom requis").max(255),
|
||||
feedType: z.enum(["veille", "aap"]),
|
||||
defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).optional(),
|
||||
defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).optional(),
|
||||
autoRules: z.array(z.object({
|
||||
keyword: z.string(),
|
||||
value: z.string(),
|
||||
})).optional(),
|
||||
isActive: z.boolean().optional().default(true),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const id = await createRssFeed({
|
||||
url: input.url,
|
||||
name: input.name,
|
||||
feedType: input.feedType,
|
||||
defaultTypeVeille: input.defaultTypeVeille ?? null,
|
||||
defaultCategorieAap: input.defaultCategorieAap ?? null,
|
||||
autoRules: input.autoRules ?? null,
|
||||
isActive: input.isActive ?? true,
|
||||
});
|
||||
return { id };
|
||||
}),
|
||||
|
||||
// Modifier un flux
|
||||
update: adminProcedure
|
||||
.input(z.object({
|
||||
id: z.number().int().positive(),
|
||||
url: z.string().url("URL invalide").optional(),
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
feedType: z.enum(["veille", "aap"]).optional(),
|
||||
defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).nullable().optional(),
|
||||
defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).nullable().optional(),
|
||||
autoRules: z.array(z.object({
|
||||
keyword: z.string(),
|
||||
value: z.string(),
|
||||
})).nullable().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
await updateRssFeed(id, data);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Supprimer un flux
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.number().int().positive() }))
|
||||
.mutation(async ({ input }) => {
|
||||
await deleteRssFeed(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Activer / désactiver un flux
|
||||
toggleActive: adminProcedure
|
||||
.input(z.object({ id: z.number().int().positive(), isActive: z.boolean() }))
|
||||
.mutation(async ({ input }) => {
|
||||
await updateRssFeed(input.id, { isActive: input.isActive });
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Lire les paramètres globaux RSS
|
||||
getSettings: protectedProcedure.query(async () => {
|
||||
return getRssSettings();
|
||||
}),
|
||||
|
||||
// Sauvegarder les paramètres globaux RSS
|
||||
saveSettings: adminProcedure
|
||||
.input(z.object({
|
||||
fetchIntervalMinutes: z.number().int().min(5).max(10080).optional(),
|
||||
scheduledTime: z.string().regex(/^\d{2}:\d{2}$/, "Format HH:MM requis").optional(),
|
||||
fetchMode: z.enum(["interval", "scheduled"]).optional(),
|
||||
autoFetchEnabled: z.boolean().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
await saveRssSettings(input);
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
Reference in New Issue
Block a user