Checkpoint: Implémentation du résumé automatique généré par l'IA pour chaque article RSS (veille et AAP), marquage lu/non lu avec point bleu et fond teinté, bouton "Tout marquer comme lu", compteurs non lus dans la sidebar et dans les titres de page. Migration BDD : table article_reads + colonne iaResume dans veille_items et aap_items.
This commit is contained in:
@@ -10,6 +10,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
LayoutGrid,
|
||||
List,
|
||||
Eye,
|
||||
ExternalLink,
|
||||
Calendar,
|
||||
MapPin,
|
||||
@@ -53,6 +54,7 @@ interface AAPItem {
|
||||
iaCategorie: string | null;
|
||||
iaClassifiedBy: "ia" | "rules" | null;
|
||||
iaReason: string | null;
|
||||
iaResume: string | null;
|
||||
}
|
||||
|
||||
const CAT_COLORS: Record<string, string> = {
|
||||
@@ -110,6 +112,20 @@ export default function AAPDashboard() {
|
||||
const { user } = useLocalAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
// Marquage lu/non lu
|
||||
const [readIds, setReadIds] = useState<Set<number>>(new Set());
|
||||
const markAsReadMutation = trpc.aap.markAsRead.useMutation({
|
||||
onSuccess: (_, vars) => {
|
||||
setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; });
|
||||
},
|
||||
});
|
||||
const markAllAsReadMutation = trpc.aap.markAllAsRead.useMutation({
|
||||
onSuccess: () => { utils.aap.unreadCount.invalidate(); },
|
||||
});
|
||||
const unreadCountQuery = trpc.aap.unreadCount.useQuery();
|
||||
const unreadCount = unreadCountQuery.data?.count ?? 0;
|
||||
|
||||
const purgeMutation = trpc.aap.purge.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
||||
@@ -175,12 +191,20 @@ export default function AAPDashboard() {
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Target size={22} className="text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground">Appels à Projets</h1>
|
||||
{unreadCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full bg-primary text-primary-foreground text-[11px] font-bold">{unreadCount}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Handicap, Personnes Âgées, Enfance, Précarité, Sanitaire et Autre
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{unreadCount > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={() => markAllAsReadMutation.mutate()} disabled={markAllAsReadMutation.isPending} className="gap-2 text-muted-foreground">
|
||||
Tout marquer comme lu
|
||||
</Button>
|
||||
)}
|
||||
<Button variant={viewMode === "list" ? "default" : "outline"} size="sm" onClick={() => setViewMode("list")} className="gap-2">
|
||||
<List size={15} />Liste
|
||||
</Button>
|
||||
@@ -256,9 +280,9 @@ export default function AAPDashboard() {
|
||||
<p className="text-muted-foreground/60 text-sm mt-1">Modifiez vos filtres ou importez des données</p>
|
||||
</div>
|
||||
) : viewMode === "list" ? (
|
||||
<AAPListView items={items} />
|
||||
<AAPListView items={items} readIds={readIds} onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })} />
|
||||
) : (
|
||||
<AAPGridView items={items} />
|
||||
<AAPGridView items={items} readIds={readIds} onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })} />
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -279,7 +303,7 @@ export default function AAPDashboard() {
|
||||
|
||||
// ─── Vue Liste ────────────────────────────────────────────────────────────────
|
||||
|
||||
function AAPListView({ items }: { items: AAPItem[] }) {
|
||||
function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set<number>; onMarkRead: (id: number) => void }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-border overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
@@ -298,10 +322,18 @@ function AAPListView({ items }: { items: AAPItem[] }) {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{items.map((item, idx) => (
|
||||
<tr key={item.id} className={cn("hover:bg-muted/30 transition-colors border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "border-l-transparent")}>
|
||||
<tr key={item.id} className={cn("hover:bg-muted/30 transition-colors border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "border-l-transparent", !readIds.has(item.id) && "bg-blue-50/30")}>
|
||||
<td className="px-4 py-3 text-muted-foreground/50 text-xs">{idx + 1}</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium text-foreground line-clamp-2 max-w-sm leading-snug">{item.titre}</p>
|
||||
<div className="flex items-start gap-2 max-w-sm">
|
||||
{!readIds.has(item.id) && (
|
||||
<span className="mt-1.5 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />
|
||||
)}
|
||||
<div>
|
||||
<p className={cn("font-medium line-clamp-2 leading-snug", readIds.has(item.id) ? "text-muted-foreground" : "text-foreground")}>{item.titre}</p>
|
||||
{item.iaResume && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{item.iaResume}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline" className={cn("text-xs", CAT_COLORS[item.iaCategorie || item.categorie])}>
|
||||
@@ -313,11 +345,18 @@ function AAPListView({ items }: { items: AAPItem[] }) {
|
||||
<td className="px-4 py-3 text-muted-foreground text-xs whitespace-nowrap">{formatDate(item.datePublication) || "—"}</td>
|
||||
<td className="px-4 py-3"><ClotureStatus date={item.dateCloture} /></td>
|
||||
<td className="px-4 py-3">
|
||||
{item.lien && (
|
||||
<a href={item.lien} target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent/80 transition-colors">
|
||||
<ExternalLink size={15} />
|
||||
</a>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{!readIds.has(item.id) && (
|
||||
<button onClick={() => onMarkRead(item.id)} className="text-muted-foreground hover:text-primary transition-colors" title="Marquer comme lu">
|
||||
<Eye size={13} />
|
||||
</button>
|
||||
)}
|
||||
{item.lien && (
|
||||
<a href={item.lien} target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent/80 transition-colors">
|
||||
<ExternalLink size={15} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -330,11 +369,11 @@ function AAPListView({ items }: { items: AAPItem[] }) {
|
||||
|
||||
// ─── Vue Vignettes ────────────────────────────────────────────────────────────
|
||||
|
||||
function AAPGridView({ items }: { items: AAPItem[] }) {
|
||||
function AAPGridView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set<number>; onMarkRead: (id: number) => void }) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{items.map((item) => (
|
||||
<Card key={item.id} className={cn("group hover:shadow-md transition-all duration-200 border-border overflow-hidden border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "")}>
|
||||
<Card key={item.id} className={cn("group hover:shadow-md transition-all duration-200 border-border overflow-hidden border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "", !readIds.has(item.id) && "ring-1 ring-primary/20")}>
|
||||
<CardHeader className="pb-2 pt-4 px-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<Badge variant="outline" className={cn("text-xs flex-shrink-0", CAT_COLORS[item.iaCategorie || item.categorie])}>
|
||||
@@ -346,7 +385,10 @@ function AAPGridView({ items }: { items: AAPItem[] }) {
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm text-foreground leading-snug line-clamp-3 mt-2">{item.titre}</h3>
|
||||
<div className="flex items-start gap-1.5 mt-2">
|
||||
{!readIds.has(item.id) && <span className="mt-1 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />}
|
||||
<h3 className={cn("font-semibold text-sm leading-snug line-clamp-3", readIds.has(item.id) ? "text-muted-foreground" : "text-foreground")}>{item.titre}</h3>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4 space-y-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user