From f449d945e9124a7daff509658945107670f5e2f5 Mon Sep 17 00:00:00 2001 From: Michele Date: Mon, 9 Mar 2026 00:33:00 +0100 Subject: [PATCH] feat(03-02): picker Swipe File nel form Genera Calendario + mark-used - CalendarRequest in types.ts: aggiunto topic_overrides?: Record - hooks.ts: aggiunto useMarkSwipeUsed hook (POST /swipe/{id}/mark-used) - GenerateCalendar.tsx: sezione Topic Override con griglia 13 slot - Bottone "Da Swipe File" per aprire picker inline per ogni slot - Picker mostra lista idee con nicchia badge e badge Usato - Selezione assegna topic allo slot e chiama mark-used - Bottone X per rimuovere override da uno slot - Override inclusi in CalendarRequest.topic_overrides al submit - Riepilogo counter override selezionati --- frontend/src/api/hooks.ts | 11 ++ frontend/src/pages/GenerateCalendar.tsx | 228 +++++++++++++++++++++++- frontend/src/types.ts | 1 + 3 files changed, 234 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts index af6f47c..32c5611 100644 --- a/frontend/src/api/hooks.ts +++ b/frontend/src/api/hooks.ts @@ -284,3 +284,14 @@ export function useDeleteSwipeItem() { }, }) } + +/** Marca un'idea dello Swipe File come "usata". */ +export function useMarkSwipeUsed() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (id) => apiPost(`/swipe/${id}/mark-used`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['swipe'] }) + }, + }) +} diff --git a/frontend/src/pages/GenerateCalendar.tsx b/frontend/src/pages/GenerateCalendar.tsx index 34b8473..acafb70 100644 --- a/frontend/src/pages/GenerateCalendar.tsx +++ b/frontend/src/pages/GenerateCalendar.tsx @@ -3,27 +3,124 @@ * * Flusso: * 1. Utente compila form (obiettivo + settimane) - * 2. Submit chiama POST /api/generate/bulk → ritorna {job_id} (202 Accepted) - * 3. Mostra ProgressIndicator con polling ogni 2s su job_id - * 4. Quando il job completa → navigate a /risultati/:jobId + * 2. Opzionale: seleziona topic override dallo Swipe File per slot specifici + * 3. Submit chiama POST /api/generate/bulk → ritorna {job_id} (202 Accepted) + * 4. Mostra ProgressIndicator con polling ogni 2s su job_id + * 5. Quando il job completa → navigate a /risultati/:jobId * * Il pulsante Genera è disabilitato se API key non configurata. */ -import { AlertTriangle, Loader2 } from 'lucide-react' +import { AlertTriangle, Lightbulb, Loader2, X } from 'lucide-react' import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' -import { useGenerateCalendar, useSettings, useSettingsStatus } from '../api/hooks' +import { + useGenerateCalendar, + useMarkSwipeUsed, + useSettings, + useSettingsStatus, + useSwipeItems, +} from '../api/hooks' import { ProgressIndicator } from '../components/ProgressIndicator' -import type { CalendarRequest } from '../types' +import type { CalendarRequest, SwipeItem } from '../types' + +// --------------------------------------------------------------------------- +// Tipi locali +// --------------------------------------------------------------------------- + +interface SlotOverride { + topic: string + swipeId: string +} + +// --------------------------------------------------------------------------- +// Componente picker inline Swipe File +// --------------------------------------------------------------------------- + +interface SwipePickerProps { + slotIndex: number + items: SwipeItem[] + onSelect: (item: SwipeItem) => void + onClose: () => void +} + +function SwipePicker({ slotIndex, items, onSelect, onClose }: SwipePickerProps) { + return ( +
+
+ + Slot {slotIndex + 1} — Seleziona dal tuo Swipe File + + +
+ + {items.length === 0 ? ( +
+ +

Nessuna idea nel tuo Swipe File.

+ + Aggiungi idee allo Swipe File + +
+ ) : ( +
+ {items.map((item) => ( + + ))} +
+ )} +
+ ) +} + +// --------------------------------------------------------------------------- +// Componente principale +// --------------------------------------------------------------------------- export function GenerateCalendar() { const navigate = useNavigate() const { data: status } = useSettingsStatus() const { data: settings } = useSettings() + const { data: swipeData } = useSwipeItems() const generateMutation = useGenerateCalendar() + const markUsed = useMarkSwipeUsed() const apiKeyOk = status?.api_key_configured ?? false + const swipeItems = swipeData?.items ?? [] // Stato del form const [obiettivo, setObiettivo] = useState('') @@ -32,9 +129,43 @@ export function GenerateCalendar() { const [tono, setTono] = useState('') const [customNicchie, setCustomNicchie] = useState(false) + // Stato topic override + const [topicOverrides, setTopicOverrides] = useState>({}) + const [showSwipePicker, setShowSwipePicker] = useState(null) + // Job ID quando la generazione parte const [jobId, setJobId] = useState(null) + // --------------------------------------------------------------------------- + // Handlers override + // --------------------------------------------------------------------------- + + function handleSelectSwipeItem(slotIndex: number, item: SwipeItem) { + setTopicOverrides((prev) => ({ + ...prev, + [slotIndex]: { topic: item.topic, swipeId: item.id }, + })) + setShowSwipePicker(null) + // Marca come usata (fire and forget) + markUsed.mutate(item.id) + } + + function handleRemoveOverride(slotIndex: number) { + setTopicOverrides((prev) => { + const next = { ...prev } + delete next[slotIndex] + return next + }) + } + + function handleOpenPicker(slotIndex: number) { + setShowSwipePicker((prev) => (prev === slotIndex ? null : slotIndex)) + } + + // --------------------------------------------------------------------------- + // Submit + // --------------------------------------------------------------------------- + async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!apiKeyOk || !obiettivo.trim()) return @@ -44,6 +175,12 @@ export function GenerateCalendar() { settimane, frequenza_post: settings?.frequenza_post ?? 3, nicchie: customNicchie ? settings?.nicchie_attive : undefined, + topic_overrides: + Object.keys(topicOverrides).length > 0 + ? Object.fromEntries( + Object.entries(topicOverrides).map(([k, v]) => [parseInt(k), v.topic]) + ) + : undefined, } try { @@ -186,6 +323,85 @@ export function GenerateCalendar() { /> + {/* Topic Override dallo Swipe File */} +
+
+

Topic Override (opzionale)

+

+ Seleziona topic dallo Swipe File per slot specifici. Gli slot senza override useranno la generazione automatica. +

+
+ + {/* Griglia 13 slot */} +
+ {Array.from({ length: 13 }, (_, i) => i).map((slotIndex) => { + const override = topicOverrides[slotIndex] + const isPickerOpen = showSwipePicker === slotIndex + + return ( +
+
+
+ Slot {slotIndex + 1} + {override && ( + + )} +
+ + {override ? ( +

+ {override.topic} +

+ ) : ( + + )} +
+ + {/* Picker inline */} + {isPickerOpen && ( +
+ handleSelectSwipeItem(slotIndex, item)} + onClose={() => setShowSwipePicker(null)} + /> +
+ )} +
+ ) + })} +
+ + {/* Riepilogo override selezionati */} + {Object.keys(topicOverrides).length > 0 && ( +

+ {Object.keys(topicOverrides).length} override selezionati — questi slot useranno i topic dello Swipe File. +

+ )} +
+ {/* Nicchie */}