diff --git a/messages/de.json b/messages/de.json index c09d62ccd..654e9d673 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog beschreibt deine eigenen erfassten Daten — es ist keine klinische Bewertung und keine Diagnose. Sprich mit deiner Ärztin oder deinem Arzt über alles, was dich beunruhigt." - } + }, + "settings": { + "overviewTitle": "Übersicht anordnen", + "overviewDescription": "Abschnitte ein- oder ausblenden und ihre Reihenfolge auf der Insights-Übersicht festlegen." + }, + "pillOrder": { + "title": "Navigations-Pills sortieren", + "description": "Ziehe die Pills in die Reihenfolge, in der sie oben in den Insights erscheinen sollen.", + "dragHandle": "Verschieben", + "saveSuccess": "Reihenfolge der Pills gespeichert.", + "saveError": "Speichern der Pill-Reihenfolge fehlgeschlagen.", + "save": "Speichern", + "empty": "Noch keine Navigations-Pills verfügbar." + }, + "customize": "Insights anpassen", + "regenerateError": "Aktualisierung läuft noch — bitte gleich erneut versuchen." }, "thresholds": { "defaultLabel": "Default", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Freigabe", "description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen." + }, + "insights": { + "title": "Insights", + "description": "Reihenfolge der Übersicht und der Navigations-Pills." } }, "profile": "Profil", diff --git a/messages/en.json b/messages/en.json index a8d9cf9a4..d5bc6e7fe 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog describes your own tracked data — it is not a clinical assessment or diagnosis. Talk to your doctor about anything that concerns you." - } + }, + "settings": { + "overviewTitle": "Arrange the overview", + "overviewDescription": "Show or hide sections and set their order on the Insights overview." + }, + "pillOrder": { + "title": "Sort the navigation pills", + "description": "Drag the pills into the order they should appear in along the top of Insights.", + "dragHandle": "Reorder", + "saveSuccess": "Pill order saved.", + "saveError": "Saving the pill order failed.", + "save": "Save", + "empty": "No navigation pills available yet." + }, + "customize": "Customize Insights", + "regenerateError": "Still refreshing — please try again in a moment." }, "thresholds": { "defaultLabel": "Default", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Sharing", "description": "Create a read-only link to share your health record with a clinician." + }, + "insights": { + "title": "Insights", + "description": "Overview and navigation-pill order." } }, "profile": "Profile", diff --git a/messages/es.json b/messages/es.json index d11008a7e..acfecf876 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog describe tus propios datos registrados — no es una evaluación clínica ni un diagnóstico. Habla con tu médico sobre cualquier cosa que te preocupe." - } + }, + "settings": { + "overviewTitle": "Organizar la vista general", + "overviewDescription": "Muestra u oculta secciones y define su orden en la vista general de Análisis." + }, + "pillOrder": { + "title": "Ordenar las pestañas de navegación", + "description": "Arrastra las pestañas al orden en que deben aparecer en la parte superior de Análisis.", + "dragHandle": "Reordenar", + "saveSuccess": "Orden de las pestañas guardado.", + "saveError": "No se pudo guardar el orden de las pestañas.", + "save": "Guardar", + "empty": "Aún no hay pestañas de navegación disponibles." + }, + "customize": "Personalizar Análisis", + "regenerateError": "Aún se está actualizando: inténtalo de nuevo en un momento." }, "thresholds": { "defaultLabel": "Por defecto", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Compartir", "description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico." + }, + "insights": { + "title": "Análisis", + "description": "Orden de la vista general y de las pestañas de navegación." } }, "profile": "Perfil", diff --git a/messages/fr.json b/messages/fr.json index ab8aaf052..0c3954a1b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog décrit vos propres données suivies — il ne s'agit pas d'une évaluation clinique ni d'un diagnostic. Parlez à votre médecin de tout ce qui vous préoccupe." - } + }, + "settings": { + "overviewTitle": "Organiser la vue d'ensemble", + "overviewDescription": "Affichez ou masquez des sections et définissez leur ordre sur la vue d'ensemble des Analyses." + }, + "pillOrder": { + "title": "Trier les onglets de navigation", + "description": "Faites glisser les onglets dans l'ordre où ils doivent apparaître en haut des Analyses.", + "dragHandle": "Réorganiser", + "saveSuccess": "Ordre des onglets enregistré.", + "saveError": "L'enregistrement de l'ordre des onglets a échoué.", + "save": "Enregistrer", + "empty": "Aucun onglet de navigation disponible pour le moment." + }, + "customize": "Personnaliser les Analyses", + "regenerateError": "Actualisation en cours — réessayez dans un instant." }, "thresholds": { "defaultLabel": "Par défaut", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Partage", "description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé." + }, + "insights": { + "title": "Analyses", + "description": "Ordre de la vue d'ensemble et des onglets de navigation." } }, "profile": "Profil", diff --git a/messages/it.json b/messages/it.json index 1df6a9855..093d200f5 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog descrive i tuoi dati registrati — non è una valutazione clinica né una diagnosi. Parla con il tuo medico di qualsiasi cosa ti preoccupi." - } + }, + "settings": { + "overviewTitle": "Organizza la panoramica", + "overviewDescription": "Mostra o nascondi le sezioni e definisci il loro ordine nella panoramica degli Approfondimenti." + }, + "pillOrder": { + "title": "Ordina le schede di navigazione", + "description": "Trascina le schede nell'ordine in cui devono comparire in alto negli Approfondimenti.", + "dragHandle": "Riordina", + "saveSuccess": "Ordine delle schede salvato.", + "saveError": "Salvataggio dell'ordine delle schede non riuscito.", + "save": "Salva", + "empty": "Nessuna scheda di navigazione ancora disponibile." + }, + "customize": "Personalizza gli Approfondimenti", + "regenerateError": "Aggiornamento ancora in corso — riprova tra un momento." }, "thresholds": { "defaultLabel": "Predefinito", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Condivisione", "description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico." + }, + "insights": { + "title": "Approfondimenti", + "description": "Ordine della panoramica e delle schede di navigazione." } }, "profile": "Profilo", diff --git a/messages/pl.json b/messages/pl.json index bd5133174..4f43e1734 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3041,7 +3041,22 @@ }, "disclaimer": { "footer": "HealthLog opisuje Twoje własne zarejestrowane dane — nie jest oceną kliniczną ani diagnozą. Porozmawiaj z lekarzem o wszystkim, co Cię niepokoi." - } + }, + "settings": { + "overviewTitle": "Uporządkuj przegląd", + "overviewDescription": "Pokaż lub ukryj sekcje i ustaw ich kolejność w przeglądzie Analiz." + }, + "pillOrder": { + "title": "Sortuj pigułki nawigacyjne", + "description": "Przeciągnij pigułki w kolejności, w jakiej mają się pojawiać u góry Analiz.", + "dragHandle": "Zmień kolejność", + "saveSuccess": "Zapisano kolejność pigułek.", + "saveError": "Zapisanie kolejności pigułek nie powiodło się.", + "save": "Zapisz", + "empty": "Brak dostępnych pigułek nawigacyjnych." + }, + "customize": "Dostosuj Analizy", + "regenerateError": "Trwa odświeżanie — spróbuj ponownie za chwilę." }, "thresholds": { "defaultLabel": "Domyślne", @@ -3429,6 +3444,10 @@ "sharing": { "title": "Udostępnianie", "description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi." + }, + "insights": { + "title": "Analizy", + "description": "Kolejność przeglądu i pigułek nawigacyjnych." } }, "profile": "Profil", diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index 38e5fadae..ad7eb5f22 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -8,6 +8,7 @@ import { AiSection } from "@/components/settings/ai-section"; import { ApiSection } from "@/components/settings/api-section"; import { DashboardSection } from "@/components/settings/dashboard-section"; import { ExportSection } from "@/components/settings/export-section"; +import { InsightsSection } from "@/components/settings/insights-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; import { SectionPlaceholder } from "@/components/settings/section-placeholder"; @@ -49,6 +50,7 @@ const SECTION_COMPONENTS: Record< integrations: IntegrationsSection, notifications: NotificationsSection, dashboard: DashboardSection, + insights: InsightsSection, thresholds: ThresholdsSection, sources: () => , api: ApiSection, diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index dae7fd79a..b55afb91f 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -35,7 +35,7 @@ function renderShell(props: { } describe("SETTINGS_SECTION_SLUGS", () => { - it("declares the twelve sections", () => { + it("declares every section", () => { // Order matters — `generateStaticParams()` and the sidebar derive their // ordering from this constant, so a reorder is a behaviour change. // v1.4.3 split the dashboard panel: layout stays under `dashboard`, @@ -48,11 +48,14 @@ describe("SETTINGS_SECTION_SLUGS", () => { // IW-D merged it into `thresholds`. // v1.8.7.1 — `sources` (Sources) is its own slug again, sitting // between `thresholds` (Targets) and `ai`. + // v1.15.18 — `insights` sits between `dashboard` and `thresholds`: the + // overview-arrange + pill-sort customise surface for `/insights`. expect([...SETTINGS_SECTION_SLUGS]).toEqual([ "account", "integrations", "notifications", "dashboard", + "insights", "thresholds", "sources", "ai", diff --git a/src/components/settings/insights-overview-arrange-section.tsx b/src/components/settings/insights-overview-arrange-section.tsx new file mode 100644 index 000000000..35b3fd4b9 --- /dev/null +++ b/src/components/settings/insights-overview-arrange-section.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useMemo } from "react"; + +import { Loader2 } from "lucide-react"; + +import { useTranslations } from "@/lib/i18n/context"; +import { useAuth } from "@/hooks/use-auth"; +import { useFeatureFlags } from "@/hooks/use-feature-flags"; +import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout"; +import { InsightsEditMode } from "@/components/insights/insights-edit-mode"; +import { type InsightsSectionId } from "@/lib/insights-layout"; + +/** + * v1.15.18 — overview-arrange block for the Insights settings section. + * + * Embeds the existing v1.15.11 `` (section show/hide + order, + * persisted to `insightsLayoutJson.sections`) as an always-open surface. There + * is no "Anpassen" toggle here — Settings IS the customise surface — so `onClose` + * is a no-op (after a "Fertig" save the layout query has already settled into the + * shared cache; the editor simply stays mounted). + * + * `gatedOffSectionIds` mirrors the mother page's gate logic so a section whose + * feature flag / data gate is off renders its row disabled-with-a-hint rather + * than offering a toggle that does nothing. + */ +export function InsightsOverviewArrangeSection({ id }: { id?: string }) { + const { t } = useTranslations(); + const { isAuthenticated, user } = useAuth(); + const flags = useFeatureFlags(); + const { layout, isLoading } = useInsightsLayoutQuery(isAuthenticated); + + const gatedOffSectionIds = useMemo(() => { + const gated = new Set(); + if (!flags.briefing) { + gated.add("daily-briefing"); + gated.add("period-review"); + } + if (!user?.cycleTrackingEnabled) { + gated.add("cycle-summary"); + } + return gated; + }, [flags.briefing, user?.cycleTrackingEnabled]); + + return ( +
+
+

+ {t("insights.settings.overviewTitle")} +

+

+ {t("insights.settings.overviewDescription")} +

+
+ + {/* Gate the editor mount until the layout GET settles. The editor seeds + its draft once from `layout` on mount, which is the canonical default + while in flight; mounting it early would let a "Fertig" save flush + defaults over the user's real saved layout (the same QA-L1 gate the + mother page applies to its "Anpassen" toggle). */} + {isLoading ? ( +
+ +
+ ) : ( + {}} + /> + )} +
+ ); +} diff --git a/src/components/settings/insights-pill-order-section.tsx b/src/components/settings/insights-pill-order-section.tsx new file mode 100644 index 000000000..186042d10 --- /dev/null +++ b/src/components/settings/insights-pill-order-section.tsx @@ -0,0 +1,333 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { GripVertical, Loader2 } from "lucide-react"; +import { + DndContext, + KeyboardSensor, + PointerSensor, + closestCenter, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { prefersReducedMotion } from "@/lib/charts/reduced-motion"; +import { useTranslations } from "@/lib/i18n/context"; +import { useAuth } from "@/hooks/use-auth"; +import { queryKeys } from "@/lib/query-keys"; +import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout"; +import { reorderById } from "@/lib/insights-layout-reorder"; +import { + type InsightsLayout, + type InsightsTileConfig, +} from "@/lib/insights-layout"; +import { + MANAGER_GROUP_ORDER, + SUB_PAGE_MANAGER_GROUP_SLUGS, + type ManagerGroup, +} from "@/lib/insights/sub-page-metric"; +import { SUB_PAGE_TABS } from "@/components/insights/insights-tab-strip"; + +/** + * v1.15.18 — dedicated pill-sort control for the Insights settings section. + * + * The Insights nav PILLS persist to `insightsLayoutJson.tiles[].order`, a field + * that is ALREADY separate from the overview `sections[].order` in layout v2. + * Until now the pill order could only be changed implicitly through the + * overview "Anpassen" → "Kacheln verwalten" disclosure (the Vitals row), which + * the maintainer found unintuitive. This is the explicit surface: drag the + * pills, "Speichern" PUTs the merged `{ tiles }` blob to `/api/insights/layout`, + * which the tab strip + overview share via `queryKeys.insightsLayout()`. + * + * Drag mirrors `insights-edit-mode` — each manager group is its OWN + * `DndContext` + `SortableContext` so a drag never crosses a group boundary. + * The pills here are NOT visibility toggles; visibility lives on the overview + * "Übersicht anordnen" block so the two concerns stay distinct. + */ + +const MANAGER_GROUP_HEADER_KEYS: Record = { + vitals: "insights.tabStrip.vitalsParent.label", + body: "insights.tabStrip.bodyParent.label", + activity: "insights.tabStrip.activityParent.label", + sleep: "insights.editMode.groupSleep", + cardiovascular: "insights.tabStrip.cardiovascularParent.label", + hearing: "insights.tabStrip.hearingParent.label", + environment: "insights.tabStrip.environmentParent.label", + metabolic: "insights.tabStrip.metabolicParent.label", + mood: "insights.editMode.groupMood", + events: "insights.editMode.groupEvents", +}; + +/** Stable id-order fingerprint used to detect when the server copy changed. */ +function orderSignature(tiles: readonly InsightsTileConfig[]): string { + return [...tiles] + .sort((a, b) => a.order - b.order) + .map((tile) => tile.id) + .join(","); +} + +export function InsightsPillOrderSection({ id }: { id?: string }) { + const { t } = useTranslations(); + const { isAuthenticated } = useAuth(); + const queryClient = useQueryClient(); + const { layout, isLoading, isSuccess } = + useInsightsLayoutQuery(isAuthenticated); + + // Local draft of the tiles, seeded from the resolved layout and re-seeded in + // render (the React-sanctioned "adjust state on prop change" pattern, no + // effect) whenever the server copy's id-order changes — e.g. after a save or + // once the GET settles. Tracking the last-seeded signature in STATE (not a + // ref) keeps the reseed render-safe and idempotent while leaving in-flight + // drag edits untouched: the baseline only advances when the *server* order + // differs from what we last seeded from. + const serverSignature = orderSignature(layout.tiles); + const [seededSignature, setSeededSignature] = useState(serverSignature); + const [draftTiles, setDraftTiles] = useState( + () => [...layout.tiles].sort((a, b) => a.order - b.order), + ); + if (isSuccess && serverSignature !== seededSignature) { + setSeededSignature(serverSignature); + setDraftTiles([...layout.tiles].sort((a, b) => a.order - b.order)); + } + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const saveMutation = useMutation({ + mutationFn: async (tiles: InsightsTileConfig[]) => { + const res = await fetch("/api/insights/layout", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + version: 2, + // Preserve the overview section order/visibility verbatim — this + // surface owns the PILL order only. + sections: layout.sections, + tiles, + }), + }); + if (!res.ok) throw new Error("save failed"); + return (await res.json()).data as InsightsLayout; + }, + onSuccess: (saved) => { + queryClient.setQueryData(queryKeys.insightsLayout(), saved); + void queryClient.invalidateQueries({ + queryKey: queryKeys.insightsLayout(), + }); + toast.success(t("insights.pillOrder.saveSuccess")); + }, + onError: () => toast.error(t("insights.pillOrder.saveError")), + }); + + const busy = saveMutation.isPending; + + // Group the draft tiles the same way the tab strip + manager do, so each + // group's pills sort within their own `SortableContext`. + const tilesByGroup = useMemo(() => { + const byId = new Map( + draftTiles.map((tile) => [tile.id, tile]), + ); + return MANAGER_GROUP_ORDER.map((group) => { + const rows = SUB_PAGE_MANAGER_GROUP_SLUGS[group] + .map((slug) => { + const cfg = byId.get(slug); + return { + id: slug as string, + labelKey: SUB_PAGE_TABS[slug].labelKey, + order: cfg?.order ?? Number.MAX_SAFE_INTEGER, + }; + }) + .sort((a, b) => a.order - b.order); + return { group, rows }; + }).filter((g) => g.rows.length > 0); + }, [draftTiles]); + + function handleGroupDragEnd(group: ManagerGroup, event: DragEndEvent) { + const { active, over } = event; + if (!over || active.id === over.id) return; + const groupRows = tilesByGroup.find((g) => g.group === group)?.rows ?? []; + const groupMemberIds = new Set(groupRows.map((r) => r.id)); + const subset = groupRows.map((r) => ({ id: r.id, order: r.order })); + const reordered = reorderById(subset, String(active.id), String(over.id)); + const reorderedIds = reordered.map((r) => r.id); + + setDraftTiles((tiles) => { + // Substitute this group's slots in their new relative order, leaving + // every other tile (and its order) untouched. Walk the tile list and, + // at each group-member slot, take the next id from the reordered list. + let cursor = 0; + return tiles.map((tile) => { + if (!groupMemberIds.has(tile.id)) return tile; + const nextId = reorderedIds[cursor++]; + const replacement = tiles.find((tt) => tt.id === nextId); + // Keep this slot's `order` and `visible`; swap the id into position. + return replacement + ? { ...replacement, order: tile.order } + : tile; + }); + }); + } + + const dirty = useMemo(() => { + const original = [...layout.tiles].sort((a, b) => a.order - b.order); + if (original.length !== draftTiles.length) return true; + const draftByOrder = [...draftTiles].sort((a, b) => a.order - b.order); + return original.some((tt, i) => tt.id !== draftByOrder[i]?.id); + }, [layout.tiles, draftTiles]); + + return ( +
+
+
+

+ {t("insights.pillOrder.title")} +

+

+ {t("insights.pillOrder.description")} +

+
+ +
+ + {isLoading ? ( +

+ +

+ ) : tilesByGroup.length === 0 ? ( +

+ {t("insights.pillOrder.empty")} +

+ ) : ( +
+ {tilesByGroup.map(({ group, rows }) => ( +
+

+ {t(MANAGER_GROUP_HEADER_KEYS[group])} +

+ handleGroupDragEnd(group, event)} + > + r.id)} + strategy={verticalListSortingStrategy} + > +
+ {rows.map((tile) => ( + + ))} +
+
+
+
+ ))} +
+ )} +
+ ); +} + +const DRAG_HANDLE_CLASS = + "text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background relative inline-flex h-11 w-11 shrink-0 cursor-grab touch-none items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none sm:h-9 sm:w-9 sm:before:absolute sm:before:inset-[-6px] sm:before:content-['']"; + +function SortablePillRow({ + id, + title, + disabled, + dragHandleLabel, +}: { + id: string; + title: string; + disabled: boolean; + dragHandleLabel: string; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = + useSortable({ id }); + + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition: prefersReducedMotion() ? "none" : transition, + }; + + return ( +
+ + + {title} + +
+ ); +} diff --git a/src/components/settings/insights-section.tsx b/src/components/settings/insights-section.tsx new file mode 100644 index 000000000..355de8554 --- /dev/null +++ b/src/components/settings/insights-section.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { InsightsOverviewArrangeSection } from "@/components/settings/insights-overview-arrange-section"; +import { InsightsPillOrderSection } from "@/components/settings/insights-pill-order-section"; +import { useTranslations } from "@/lib/i18n/context"; + +/** + * v1.15.18 — the "Insights" settings section (analogous to the Dashboard + * section). Hosts two blocks: + * 1. Arrange the OVERVIEW — section show/hide + order, persisted to + * `insightsLayoutJson.sections` (the v1.15.11 inline edit mode). + * 2. Sort the navigation PILLS — pill order, persisted to + * `insightsLayoutJson.tiles[].order` (already a separate field in v2). + * + * Both write through the same `/api/insights/layout` contract the tab strip + * and the overview share, so a save here repaints both surfaces in lockstep. + */ +export function InsightsSection() { + const { t } = useTranslations(); + + return ( +
+
+

+ {t("settings.sections.insights.title")} +

+

+ {t("settings.sections.insights.description")} +

+
+ + + +
+ ); +} diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx index 3f05ea25d..d3c70a569 100644 --- a/src/components/settings/section-placeholder.tsx +++ b/src/components/settings/section-placeholder.tsx @@ -22,6 +22,7 @@ import { Settings2, SlidersHorizontal, Sparkles, + TrendingUp, User, type LucideIcon, } from "lucide-react"; @@ -35,6 +36,7 @@ const SLUG_ICON: Record = { integrations: Link2, notifications: Bell, dashboard: LayoutDashboard, + insights: TrendingUp, thresholds: SlidersHorizontal, sources: Layers, ai: Sparkles, diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts index 76f010ef5..d476fa92d 100644 --- a/src/components/settings/section-slugs.ts +++ b/src/components/settings/section-slugs.ts @@ -18,6 +18,7 @@ export const SETTINGS_SECTION_SLUGS = [ "integrations", "notifications", "dashboard", + "insights", "thresholds", "sources", "ai", diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index 22043e34a..22a2c0ebf 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -27,6 +27,7 @@ import { Settings2, SlidersHorizontal, Sparkles, + TrendingUp, User, type LucideIcon, } from "lucide-react"; @@ -97,6 +98,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.dashboard.title", icon: LayoutDashboard, }, + { + slug: "insights", + titleKey: "settings.sections.insights.title", + icon: TrendingUp, + }, { slug: "thresholds", titleKey: "settings.sections.thresholds.title",