From cae304b7eb57836e6e1d86e5b40015ad28854b77 Mon Sep 17 00:00:00 2001
From: Amjad Alsharafi <26300843+Amjad50@users.noreply.github.com>
Date: Sun, 2 Feb 2025 11:40:21 +0800
Subject: [PATCH] Feature: Firefly widget (#4683)
Signed-off-by: Amjad Alsharafi <26300843+Amjad50@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
---
docs/widgets/services/firefly.md | 17 ++++++
docs/widgets/services/index.md | 1 +
mkdocs.yml | 1 +
public/locales/en/common.json | 4 ++
src/utils/proxy/handlers/credentialed.js | 1 +
src/widgets/components.js | 1 +
src/widgets/firefly/component.jsx | 71 ++++++++++++++++++++++++
src/widgets/firefly/widget.js | 19 +++++++
src/widgets/widgets.js | 2 +
9 files changed, 117 insertions(+)
create mode 100644 docs/widgets/services/firefly.md
create mode 100644 src/widgets/firefly/component.jsx
create mode 100644 src/widgets/firefly/widget.js
diff --git a/docs/widgets/services/firefly.md b/docs/widgets/services/firefly.md
new file mode 100644
index 000000000..38793ce29
--- /dev/null
+++ b/docs/widgets/services/firefly.md
@@ -0,0 +1,17 @@
+---
+title: Firefly III
+description: Firefly III Widget Configuration
+---
+
+Learn more about [Firefly III](https://www.firefly-iii.org/).
+
+Find your API key under `Options > Profile > OAuth > Personal Access Tokens`.
+
+Allowed fields: `["networth" ,"budget"]`.
+
+```yaml
+widget:
+ type: firefly
+ url: https://firefly.host.or.ip
+ key: personalaccesstoken.personalaccesstoken.personalaccesstoken
+```
diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md
index 894a31f6e..2e3965820 100644
--- a/docs/widgets/services/index.md
+++ b/docs/widgets/services/index.md
@@ -33,6 +33,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [ESPHome](esphome.md)
- [EVCC](evcc.md)
- [Fileflows](fileflows.md)
+- [Firefly III](firefly.md)
- [Flood](flood.md)
- [FreshRSS](freshrss.md)
- [Frigate](frigate.md)
diff --git a/mkdocs.yml b/mkdocs.yml
index fa2188ad5..958c3398c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -56,6 +56,7 @@ nav:
- widgets/services/esphome.md
- widgets/services/evcc.md
- widgets/services/fileflows.md
+ - widgets/services/firefly.md
- widgets/services/flood.md
- widgets/services/freshrss.md
- widgets/services/frigate.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 1d7380894..0674d6be3 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -702,6 +702,10 @@
"processed": "Processed",
"time": "Time"
},
+ "firefly": {
+ "networth": "Net Worth",
+ "budget": "Budget"
+ },
"grafana": {
"dashboards": "Dashboards",
"datasources": "Data Sources",
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index 01fb313b1..9a333d6fb 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -48,6 +48,7 @@ export default async function credentialedProxyHandler(req, res, map) {
"tandoor",
"pterodactyl",
"vikunja",
+ "firefly",
].includes(widget.type)
) {
headers.Authorization = `Bearer ${widget.key}`;
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 19f41d4ae..67d17d50b 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -30,6 +30,7 @@ const components = {
esphome: dynamic(() => import("./esphome/component")),
evcc: dynamic(() => import("./evcc/component")),
fileflows: dynamic(() => import("./fileflows/component")),
+ firefly: dynamic(() => import("./firefly/component")),
flood: dynamic(() => import("./flood/component")),
freshrss: dynamic(() => import("./freshrss/component")),
frigate: dynamic(() => import("./frigate/component")),
diff --git a/src/widgets/firefly/component.jsx b/src/widgets/firefly/component.jsx
new file mode 100644
index 000000000..30e495c16
--- /dev/null
+++ b/src/widgets/firefly/component.jsx
@@ -0,0 +1,71 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ const startOfMonth = new Date();
+ startOfMonth.setDate(1);
+ startOfMonth.setHours(0, 0, 0, 0);
+ const startOfMonthFormatted = startOfMonth.toISOString().split("T")[0];
+
+ const endOfMonth = new Date(startOfMonth);
+ endOfMonth.setMonth(endOfMonth.getMonth() + 1);
+ endOfMonth.setDate(0);
+ endOfMonth.setHours(23, 59, 59, 999);
+ const endOfMonthFormatted = endOfMonth.toISOString().split("T")[0];
+
+ const { data: summaryData, error: summaryError } = useWidgetAPI(widget, "summary", {
+ start: startOfMonthFormatted,
+ end: endOfMonthFormatted,
+ });
+
+ const { data: budgetData, error: budgetError } = useWidgetAPI(widget, "budgets", {
+ start: startOfMonthFormatted,
+ end: endOfMonthFormatted,
+ });
+
+ if (summaryError || budgetError) {
+ return ;
+ }
+
+ if (!summaryData || !budgetData) {
+ return (
+
+
+
+
+ );
+ }
+
+ const netWorth = Object.keys(summaryData)
+ .filter((key) => key.includes("net-worth-in"))
+ .map((key) => summaryData[key]);
+
+ let budgetValue = null;
+
+ if (budgetData.data?.length && budgetData.data[0].type === "available_budgets") {
+ const budgetAmount = parseFloat(budgetData.data[0].attributes.amount);
+ const budgetSpent = -parseFloat(budgetData.data[0].attributes.spent_in_budgets[0]?.sum ?? "0");
+ const budgetCurrency = budgetData.data[0].attributes.currency_symbol;
+
+ budgetValue = `${budgetCurrency} ${t("common.number", {
+ value: budgetSpent,
+ minimumFractionDigits: 2,
+ })} / ${budgetCurrency} ${t("common.number", {
+ value: budgetAmount,
+ minimumFractionDigits: 2,
+ })}`;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/widgets/firefly/widget.js b/src/widgets/firefly/widget.js
new file mode 100644
index 000000000..cd23504db
--- /dev/null
+++ b/src/widgets/firefly/widget.js
@@ -0,0 +1,19 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ summary: {
+ endpoint: "v1/summary/basic",
+ params: ["start", "end"],
+ },
+ budgets: {
+ endpoint: "v1/available-budgets",
+ params: ["start", "end"],
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 9d4bb935d..b78f5b9c2 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -24,6 +24,7 @@ import emby from "./emby/widget";
import esphome from "./esphome/widget";
import evcc from "./evcc/widget";
import fileflows from "./fileflows/widget";
+import firefly from "./firefly/widget";
import flood from "./flood/widget";
import freshrss from "./freshrss/widget";
import frigate from "./frigate/widget";
@@ -157,6 +158,7 @@ const widgets = {
esphome,
evcc,
fileflows,
+ firefly,
flood,
freshrss,
frigate,