Checkpoint: Ajout des modes de fréquence quotidien, hebdomadaire (avec sélecteur de jour Lun-Dim), mensuel (avec choix du jour 1-28) et intervalle (1h à 24h). Le cron est rechargé immédiatement après sauvegarde pour tous les paramètres de planification.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "3967f6c2",
|
||||
"timestamp": 1782655834971
|
||||
"version": "6c88c7af",
|
||||
"timestamp": 1782767248272
|
||||
}
|
||||
@@ -220,8 +220,10 @@ export default function SettingsPage() {
|
||||
sharepoint_token: "",
|
||||
auth_mode: "local",
|
||||
import_time: "06:00",
|
||||
fetch_mode: "scheduled",
|
||||
fetch_mode: "daily",
|
||||
fetch_interval_minutes: "1440",
|
||||
fetch_week_day: "1",
|
||||
fetch_month_day: "1",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -465,57 +467,106 @@ export default function SettingsPage() {
|
||||
<CardDescription>Configurez la fréquence et l'heure de déclenchement des mises à jour automatiques (import Excel + lecture RSS).</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
{/* Mode */}
|
||||
{/* Fréquence */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Mode de planification</Label>
|
||||
<div className="grid grid-cols-2 gap-3 max-w-md">
|
||||
{(["scheduled", "interval"] as const).map((mode) => (
|
||||
<Label className="text-sm font-medium">Fréquence de mise à jour</Label>
|
||||
<div className="grid grid-cols-2 gap-3 max-w-lg">
|
||||
{([
|
||||
{ value: "daily", label: "Quotidienne", desc: "Chaque jour à l'heure choisie" },
|
||||
{ value: "weekly", label: "Hebdomadaire", desc: "Un jour précis chaque semaine" },
|
||||
{ value: "monthly", label: "Mensuelle", desc: "Un jour précis chaque mois" },
|
||||
{ value: "interval", label: "Intervalle", desc: "Toutes les N heures" },
|
||||
] as const).map((opt) => (
|
||||
<button
|
||||
key={mode}
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => set("fetch_mode", mode)}
|
||||
onClick={() => set("fetch_mode", opt.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-3 rounded-lg border-2 text-left transition-all text-sm",
|
||||
(form.fetch_mode || "scheduled") === mode
|
||||
(form.fetch_mode || "daily") === opt.value
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<Clock className={cn("h-4 w-4", (form.fetch_mode || "scheduled") === mode ? "text-primary" : "text-muted-foreground")} />
|
||||
<Clock className={cn("h-4 w-4 shrink-0", (form.fetch_mode || "daily") === opt.value ? "text-primary" : "text-muted-foreground")} />
|
||||
<div>
|
||||
<p className="font-medium">{mode === "scheduled" ? "Heure fixe" : "Intervalle"}</p>
|
||||
<p className="text-xs text-muted-foreground">{mode === "scheduled" ? "Chaque jour à une heure précise" : "Toutes les N heures"}</p>
|
||||
<p className="font-medium">{opt.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{opt.desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Heure fixe */}
|
||||
{(form.fetch_mode || "scheduled") === "scheduled" && (
|
||||
{/* Heure (tous les modes sauf intervalle) */}
|
||||
{(form.fetch_mode || "daily") !== "interval" && (
|
||||
<div className="space-y-2 max-w-xs">
|
||||
<Label>Heure d'import quotidien</Label>
|
||||
<Label>Heure de déclenchement</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={form.import_time || "06:00"}
|
||||
onChange={(e) => set("import_time", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Prochain import : demain à {form.import_time || "06:00"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jour de la semaine (hebdomadaire) */}
|
||||
{(form.fetch_mode || "daily") === "weekly" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Jour de la semaine</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ label: "Lun", value: "1" },
|
||||
{ label: "Mar", value: "2" },
|
||||
{ label: "Mer", value: "3" },
|
||||
{ label: "Jeu", value: "4" },
|
||||
{ label: "Ven", value: "5" },
|
||||
{ label: "Sam", value: "6" },
|
||||
{ label: "Dim", value: "0" },
|
||||
].map((d) => (
|
||||
<button
|
||||
key={d.value}
|
||||
type="button"
|
||||
onClick={() => set("fetch_week_day", d.value)}
|
||||
className={cn(
|
||||
"w-12 py-2 rounded-lg border-2 text-sm font-medium transition-all",
|
||||
(form.fetch_week_day || "1") === d.value
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-border hover:border-primary/40 text-foreground"
|
||||
)}
|
||||
>
|
||||
{d.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jour du mois (mensuel) */}
|
||||
{(form.fetch_mode || "daily") === "monthly" && (
|
||||
<div className="space-y-2 max-w-xs">
|
||||
<Label>Jour du mois (1–28)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={28}
|
||||
value={form.fetch_month_day || "1"}
|
||||
onChange={(e) => set("fetch_month_day", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Maximum 28 pour garantir l'exécution tous les mois</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Intervalle */}
|
||||
{(form.fetch_mode || "scheduled") === "interval" && (
|
||||
<div className="space-y-2 max-w-xs">
|
||||
<Label>Fréquence de mise à jour</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{(form.fetch_mode || "daily") === "interval" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Intervalle de répétition</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ label: "1h", value: "60" },
|
||||
{ label: "2h", value: "120" },
|
||||
{ label: "4h", value: "240" },
|
||||
{ label: "6h", value: "360" },
|
||||
{ label: "1h", value: "60" },
|
||||
{ label: "2h", value: "120" },
|
||||
{ label: "4h", value: "240" },
|
||||
{ label: "6h", value: "360" },
|
||||
{ label: "12h", value: "720" },
|
||||
{ label: "24h", value: "1440" },
|
||||
].map((opt) => (
|
||||
@@ -524,7 +575,7 @@ export default function SettingsPage() {
|
||||
type="button"
|
||||
onClick={() => set("fetch_interval_minutes", opt.value)}
|
||||
className={cn(
|
||||
"py-2 px-3 rounded-lg border-2 text-sm font-medium transition-all",
|
||||
"w-14 py-2 rounded-lg border-2 text-sm font-medium transition-all",
|
||||
(form.fetch_interval_minutes || "1440") === opt.value
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-border hover:border-primary/40 text-foreground"
|
||||
@@ -535,7 +586,9 @@ export default function SettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Import toutes les {(form.fetch_interval_minutes || "1440") === "60" ? "heure" : `${parseInt(form.fetch_interval_minutes || "1440") / 60} heures`}
|
||||
Import toutes les {parseInt(form.fetch_interval_minutes || "1440") >= 60
|
||||
? `${parseInt(form.fetch_interval_minutes || "1440") / 60}h`
|
||||
: `${form.fetch_interval_minutes || "1440"} min`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -35,27 +35,46 @@ let cronJob: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
export async function scheduleDailyImport() {
|
||||
const importTime = (await getSetting("import_time")) || "06:00";
|
||||
const fetchMode = (await getSetting("fetch_mode")) || "scheduled";
|
||||
const fetchMode = (await getSetting("fetch_mode")) || "daily";
|
||||
const fetchIntervalMinutes = parseInt((await getSetting("fetch_interval_minutes")) || "1440", 10);
|
||||
const fetchWeekDay = (await getSetting("fetch_week_day")) || "1"; // 0=dim, 1=lun, ..., 6=sam
|
||||
const fetchMonthDay = (await getSetting("fetch_month_day")) || "1"; // 1-28
|
||||
|
||||
const [hour, minute] = importTime.split(":").map(Number);
|
||||
const h = hour ?? 6;
|
||||
const m = minute ?? 0;
|
||||
|
||||
let cronExpr: string;
|
||||
let cronLabel: string;
|
||||
|
||||
if (fetchMode === "interval") {
|
||||
const intervalMin = Math.max(60, fetchIntervalMinutes);
|
||||
if (intervalMin < 60) {
|
||||
cronExpr = `*/${intervalMin} * * * *`;
|
||||
} else if (intervalMin % 60 === 0) {
|
||||
if (intervalMin % 60 === 0) {
|
||||
const hours = intervalMin / 60;
|
||||
cronExpr = hours === 24 ? `0 0 * * *` : `0 */${hours} * * *`;
|
||||
cronExpr = hours === 24 ? `0 ${m} ${h} * * *` : `0 ${m} */${hours} * * *`;
|
||||
} else {
|
||||
cronExpr = `*/${intervalMin} * * * *`;
|
||||
cronExpr = `0 */${intervalMin} * * * *`;
|
||||
}
|
||||
console.log(`[Cron] Mode intervalle — toutes les ${intervalMin} minutes (${cronExpr})`);
|
||||
const hours = intervalMin / 60;
|
||||
cronLabel = `Mode intervalle — toutes les ${hours >= 1 ? hours + "h" : intervalMin + "min"} (${cronExpr})`;
|
||||
} else if (fetchMode === "weekly") {
|
||||
// hebdomadaire : le jour choisi à l'heure choisie
|
||||
cronExpr = `0 ${m} ${h} * * ${fetchWeekDay}`;
|
||||
const jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"];
|
||||
cronLabel = `Mode hebdomadaire — chaque ${jours[parseInt(fetchWeekDay)] ?? "lundi"} à ${importTime} (${cronExpr})`;
|
||||
} else if (fetchMode === "monthly") {
|
||||
// mensuel : le jour du mois choisi à l'heure choisie
|
||||
const day = Math.min(28, Math.max(1, parseInt(fetchMonthDay)));
|
||||
cronExpr = `0 ${m} ${h} ${day} * *`;
|
||||
cronLabel = `Mode mensuel — le ${day} de chaque mois à ${importTime} (${cronExpr})`;
|
||||
} else {
|
||||
const [hour, minute] = importTime.split(":").map(Number);
|
||||
cronExpr = `0 ${minute ?? 0} ${hour ?? 6} * * *`;
|
||||
console.log(`[Cron] Mode heure fixe — tous les jours à ${importTime} (${cronExpr})`);
|
||||
// daily (défaut)
|
||||
cronExpr = `0 ${m} ${h} * * *`;
|
||||
cronLabel = `Mode quotidien — tous les jours à ${importTime} (${cronExpr})`;
|
||||
}
|
||||
|
||||
console.log(`[Cron] ${cronLabel}`);
|
||||
|
||||
if (cronJob) {
|
||||
cronJob.stop();
|
||||
cronJob = null;
|
||||
|
||||
@@ -372,8 +372,10 @@ export const appRouter = router({
|
||||
sharepoint_token: z.string().optional(),
|
||||
auth_mode: z.enum(["local", "free"]).optional(),
|
||||
import_time: z.string().optional(),
|
||||
fetch_mode: z.enum(["scheduled", "interval"]).optional(),
|
||||
fetch_mode: z.enum(["daily", "weekly", "monthly", "interval", "scheduled"]).optional(),
|
||||
fetch_interval_minutes: z.string().optional(),
|
||||
fetch_week_day: z.string().optional(),
|
||||
fetch_month_day: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
@@ -383,7 +385,7 @@ export const appRouter = router({
|
||||
}
|
||||
await setSettings(toSave);
|
||||
// Recharger le cron si la planification a changé
|
||||
if (toSave.import_time || toSave.fetch_mode || toSave.fetch_interval_minutes) {
|
||||
if (toSave.import_time || toSave.fetch_mode || toSave.fetch_interval_minutes || toSave.fetch_week_day || toSave.fetch_month_day) {
|
||||
await scheduleDailyImport();
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
5
todo.md
5
todo.md
@@ -181,4 +181,9 @@
|
||||
- [x] Backend index.ts : lire fetch_mode et fetch_interval_minutes pour construire l'expression cron
|
||||
- [x] Backend routers.ts : exposer et sauvegarder ces deux nouveaux paramètres
|
||||
- [x] Frontend Settings.tsx : sélecteur mode (heure fixe / intervalle) + sélecteur intervalle (1h, 2h, 4h, 6h, 12h, 24h)
|
||||
- [x] Déployer en recette
|
||||
|
||||
## Fréquence de planification enrichie
|
||||
- [x] Backend index.ts : générer l'expression cron selon quotidien/hebdomadaire (jour)/mensuel/intervalle
|
||||
- [x] Frontend Settings.tsx : sélecteur fréquence (quotidien, hebdo + jour, mensuel, intervalle) + heure
|
||||
- [ ] Déployer en recette
|
||||
|
||||
Reference in New Issue
Block a user