"use client"; import { Fragment } from "react"; import type { CSSProperties } from "react"; import { RingProgress, Text } from "@mantine/core"; import { IconFileDescription, IconFileText, IconInbox, IconTag, IconUsers } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useScopedI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import type { WidgetComponentProps } from "../definition"; import classes from "./component.module.css"; const statVisibilityByOption = { showDocumentsTotal: "documentsTotal", showDocumentsInbox: "documentsInbox", showCorrespondents: "correspondents", showTags: "tags", showDocumentTypes: "documentTypes", } as const; const statIcons = { documentsTotal: IconFileText, documentsInbox: IconInbox, correspondents: IconUsers, tags: IconTag, documentTypes: IconFileDescription, } as const; const gridHiddenWhenHeroShown = new Set(["documentsTotal", "documentsInbox"]); const gridColsByWidth = [ { minWidth: 380, cols: 3 }, { minWidth: 220, cols: 2 }, { minWidth: 0, cols: 1 }, ] as const; const ringSizeByWidth = [ { minWidth: 400, size: 88 }, { minWidth: 320, size: 72 }, { minWidth: 220, size: 56 }, { minWidth: 0, size: 44 }, ] as const; const iconSizeByWidth = [ { minWidth: 320, size: 22 }, { minWidth: 200, size: 18 }, { minWidth: 0, size: 16 }, ] as const; const ringLabelSizeByRingSize = [ { minSize: 72, size: "sm" }, { minSize: 56, size: "xs" }, { minSize: 0, size: "xs" }, ] as const; const heroLayoutBySecondaryStats = { false: classes.heroExpanded, true: "", } as const; const heroVariantByRing = { true: "", false: classes.heroTextOnly, } as const; const heroPartVisibility = { ratio: "showInboxRatio", ring: "showInboxRing", } as const; export default function PaperlessNgxWidget({ integrationIds, options, width, height, displayMode = "compact", }: WidgetComponentProps<"paperlessNgx">) { const t = useScopedI18n("widget.paperlessNgx"); const { data: stats } = clientApi.widget.paperlessNgx.getStats.useQuery({ integrationId: integrationIds[0] ?? "", }); if (!stats) return ; const canShowInboxHero = options.showDocumentsInbox && options.showDocumentsTotal; const showHero = options.showInboxRatio && canShowInboxHero; const inboxProgress = computeInboxProgress(stats.documentsTotal, stats.documentsInbox); const statValues = { documentsTotal: stats.documentsTotal, documentsInbox: stats.documentsInbox, correspondents: stats.correspondentsCount, tags: stats.tagsCount, documentTypes: stats.documentTypesCount, } as const; const visibleStatKeys = Object.entries(statVisibilityByOption) .filter(([optionKey]) => options[optionKey as keyof typeof options]) .map(([, statKey]) => statKey) .filter((statKey) => !(showHero && gridHiddenWhenHeroShown.has(statKey))); const advanced = displayMode === "advanced"; const gridCols = advanced ? Math.max(1, Math.min(5, visibleStatKeys.length)) : getGridCols(width); const ringSize = advanced ? 112 : getRingSize(width); const iconSize = getIconSize(width); const ringLabelSize = getRingLabelSize(ringSize); const hasContent = showHero || visibleStatKeys.length > 0; const heroLayoutClass = heroLayoutBySecondaryStats[String(visibleStatKeys.length > 0) as keyof typeof heroLayoutBySecondaryStats]; const showInboxRing = options.showInboxRing && (advanced || height >= 120); const heroRingClass = heroVariantByRing[String(showInboxRing) as keyof typeof heroVariantByRing]; const visibleHeroParts = Object.entries(heroPartVisibility).filter( ([partKey, optionKey]) => options[optionKey as keyof typeof options] && (partKey !== "ring" || showInboxRing), ); const heroPartRenderers = { ratio: (
{t("inboxRatio")}
{stats.documentsInbox} {t("inbox")} / {stats.documentsTotal} {t("total")}
), ring: ( {inboxProgress}% } /> ), } as const; return (
{showHero && (
{visibleHeroParts.map(([partKey]) => ( {heroPartRenderers[partKey as keyof typeof heroPartRenderers]} ))}
)} {!hasContent && (
—
)} {visibleStatKeys.length > 0 && (
{visibleStatKeys.map((statKey) => { const Icon = statIcons[statKey]; return (
{statValues[statKey]} {t(statKey)}
); })}
)}
); } function computeInboxProgress(total: number, inbox: number): number { if (total <= 0) { return 0; } return Math.round((inbox / total) * 100); } function getGridCols(width: number): number { const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth); return match?.cols ?? 1; } function getRingSize(width: number): number { const match = ringSizeByWidth.find(({ minWidth }) => width >= minWidth); return match?.size ?? 44; } function getIconSize(width: number): number { const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth); return match?.size ?? 16; } function getRingLabelSize(ringSize: number): "xs" | "sm" { const match = ringLabelSizeByRingSize.find(({ minSize }) => ringSize >= minSize); return match?.size ?? "xs"; }