From 4784242a35ffa444e6bf8a74e7865b96ab7779a1 Mon Sep 17 00:00:00 2001
From: Jose Selesan
Date: Wed, 22 Jul 2026 12:31:58 -0300
Subject: [PATCH] Added estimaded salary to dashboard
---
.../20260722152402_add_settings/migration.sql | 13 ++++
apps/backend/prisma/schema.prisma | 9 +++
apps/backend/src/index.ts | 2 +
.../src/modules/settings/settings.handler.ts | 29 ++++++++
.../src/modules/settings/settings.routes.ts | 9 +++
.../src/features/dashboard/DashboardPage.tsx | 26 +++----
apps/frontend/src/lib/api.ts | 31 +++++---
apps/frontend/src/lib/queries.ts | 45 +++++++++---
.../src/routes/_authenticated/settings.tsx | 72 ++++++++++++++++++-
9 files changed, 206 insertions(+), 30 deletions(-)
create mode 100644 apps/backend/prisma/migrations/20260722152402_add_settings/migration.sql
create mode 100644 apps/backend/src/modules/settings/settings.handler.ts
create mode 100644 apps/backend/src/modules/settings/settings.routes.ts
diff --git a/apps/backend/prisma/migrations/20260722152402_add_settings/migration.sql b/apps/backend/prisma/migrations/20260722152402_add_settings/migration.sql
new file mode 100644
index 0000000..c3b1e96
--- /dev/null
+++ b/apps/backend/prisma/migrations/20260722152402_add_settings/migration.sql
@@ -0,0 +1,13 @@
+-- CreateTable
+CREATE TABLE "settings" (
+ "key" TEXT NOT NULL,
+ "value" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "settings_pkey" PRIMARY KEY ("key")
+);
+
+-- Seed
+INSERT INTO "settings" ("key", "value", "createdAt", "updatedAt")
+VALUES ('salary_multiplier', '4500', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma
index 0096a3b..659ab5c 100644
--- a/apps/backend/prisma/schema.prisma
+++ b/apps/backend/prisma/schema.prisma
@@ -122,6 +122,15 @@ model QuoteHistory {
@@map("quotes_history")
}
+model Setting {
+ key String @id
+ value String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@map("settings")
+}
+
// Expenses
enum PaymentStatus {
diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts
index bc7bd65..0e750f3 100644
--- a/apps/backend/src/index.ts
+++ b/apps/backend/src/index.ts
@@ -11,6 +11,7 @@ import { startExpenseJob } from "./modules/expenses/expense.job";
import expensesRouter from "./modules/expenses/expenses.routes";
import { startQuoteJob } from "./modules/quotes/quote.job";
import quotesRouter from "./modules/quotes/quotes.routes";
+import settingsRouter from "./modules/settings/settings.routes";
import telegramRouter from "./modules/telegram/telegram.router";
import walletsRouter from "./modules/wallets/wallets.routes";
@@ -32,6 +33,7 @@ app.use("/api/*", async (c, next) => {
});
app.route("/api/quotes", quotesRouter);
+app.route("/api/settings", settingsRouter);
app.route("/api", expensesRouter);
app.route("/api/telegram", telegramRouter);
app.route("/api/wallets", walletsRouter);
diff --git a/apps/backend/src/modules/settings/settings.handler.ts b/apps/backend/src/modules/settings/settings.handler.ts
new file mode 100644
index 0000000..0749650
--- /dev/null
+++ b/apps/backend/src/modules/settings/settings.handler.ts
@@ -0,0 +1,29 @@
+import type { Context } from "hono";
+import { cache } from "../../lib/cache";
+import { prisma } from "../../lib/prisma";
+
+export async function getSettings(c: Context) {
+ const cached = cache.get("settings:all");
+ if (cached) return c.json(cached);
+
+ const settings = await prisma.setting.findMany();
+ cache.set("settings:all", settings, 60_000);
+ return c.json(settings);
+}
+
+export async function updateSetting(c: Context) {
+ const body = await c.req.json<{ key: string; value: string }>();
+
+ if (!body.key || body.value === undefined) {
+ return c.json({ error: "key and value are required" }, 400);
+ }
+
+ const setting = await prisma.setting.upsert({
+ where: { key: body.key },
+ update: { value: body.value },
+ create: { key: body.key, value: body.value },
+ });
+
+ cache.invalidateByPrefix("settings:");
+ return c.json(setting);
+}
diff --git a/apps/backend/src/modules/settings/settings.routes.ts b/apps/backend/src/modules/settings/settings.routes.ts
new file mode 100644
index 0000000..2f72cdc
--- /dev/null
+++ b/apps/backend/src/modules/settings/settings.routes.ts
@@ -0,0 +1,9 @@
+import { Hono } from "hono";
+import { getSettings, updateSetting } from "./settings.handler";
+
+const app = new Hono();
+
+app.get("/", getSettings);
+app.put("/", updateSetting);
+
+export default app;
diff --git a/apps/frontend/src/features/dashboard/DashboardPage.tsx b/apps/frontend/src/features/dashboard/DashboardPage.tsx
index 6961e98..3957935 100644
--- a/apps/frontend/src/features/dashboard/DashboardPage.tsx
+++ b/apps/frontend/src/features/dashboard/DashboardPage.tsx
@@ -9,9 +9,9 @@ import {
import type { Quote } from "@/lib/api";
import {
useFetchQuotes,
- useMonthlyPayedTotal,
useMonthlyTotals,
useQuotes,
+ useSettings,
useWallets,
} from "@/lib/queries";
import { RelativeTime } from "@/lib/time";
@@ -131,15 +131,15 @@ export function DashboardPage() {
const bna = quotes?.find((q) => q.type === "BNA");
const now = new Date();
- const { data: monthlyExpenses } = useMonthlyPayedTotal(
- now.getFullYear(),
- now.getMonth() + 1,
- );
const { data: monthlyTotals } = useMonthlyTotals(
now.getFullYear(),
now.getMonth() + 1,
);
+ const { data: settings } = useSettings();
+ const salaryMultiplier =
+ settings?.find((s) => s.key === "salary_multiplier")?.value ?? "4500";
+
const isFetching = isLoading || isPending;
const { data: wallets } = useWallets();
@@ -173,9 +173,12 @@ export function DashboardPage() {
-
Gastos del mes
+
Sueldo
- ${priceFormatter.format(monthlyExpenses?.total ?? 0)}
+ $
+ {priceFormatter.format(
+ (belo ? Number(belo.buy) : 0) * Number(salaryMultiplier),
+ )}
Billeteras
{wallets.map((w) => (
-
+
{w.name}
-
{w.currency}
+
+ {w.currency}
+
{walletBalanceFormatter.format(w.balance)}
diff --git a/apps/frontend/src/lib/api.ts b/apps/frontend/src/lib/api.ts
index 6af7cae..2b7efa2 100644
--- a/apps/frontend/src/lib/api.ts
+++ b/apps/frontend/src/lib/api.ts
@@ -339,7 +339,12 @@ export type Wallet = {
export type WalletMovement = {
id: number;
walletId: number;
- type: "DEPOSIT" | "WITHDRAWAL" | "TRANSFER_IN" | "TRANSFER_OUT" | "ADJUSTMENT";
+ type:
+ | "DEPOSIT"
+ | "WITHDRAWAL"
+ | "TRANSFER_IN"
+ | "TRANSFER_OUT"
+ | "ADJUSTMENT";
amount: number;
description: string | null;
referenceWalletId: number | null;
@@ -390,10 +395,7 @@ export function deleteWallet(id: number): Promise
{
return mutator(`/api/wallets/${id}`, "DELETE");
}
-export function depositWallet(
- id: number,
- data: DepositInput,
-): Promise {
+export function depositWallet(id: number, data: DepositInput): Promise {
return mutator(`/api/wallets/${id}/deposit`, "POST", data);
}
@@ -418,9 +420,7 @@ export function setDefaultWallet(id: number): Promise {
return mutator(`/api/wallets/${id}/default`, "PUT");
}
-export function getWalletMovements(
- id: number,
-): Promise {
+export function getWalletMovements(id: number): Promise {
return fetcher(`/api/wallets/${id}/movements`);
}
@@ -447,6 +447,21 @@ export function getWalletMovementsFiltered(
);
}
+// Settings
+
+export type Setting = {
+ key: string;
+ value: string;
+};
+
+export function getSettings(): Promise {
+ return fetcher("/api/settings");
+}
+
+export function updateSetting(key: string, value: string): Promise {
+ return mutator("/api/settings", "PUT", { key, value });
+}
+
// Telegram
export type TelegramStatus = {
diff --git a/apps/frontend/src/lib/queries.ts b/apps/frontend/src/lib/queries.ts
index 478524b..4bd1b9f 100644
--- a/apps/frontend/src/lib/queries.ts
+++ b/apps/frontend/src/lib/queries.ts
@@ -6,23 +6,23 @@ import {
} from "@tanstack/react-query";
import {
type AdjustBalanceInput,
+ adjustBalance as adjustBalanceApi,
type CreateNonPeriodicExpenseInput,
type CreatePeriodicExpenseInput,
type CreateWalletInput,
- type DepositInput,
- type TransferInput,
- type WalletMovementsFilter,
createNonPeriodicExpense as createNonPeriodicExpenseApi,
createPeriodicExpense as createPeriodicExpenseApi,
+ createWallet as createWalletApi,
+ type DepositInput,
deletePeriodicExpense as deletePeriodicExpenseApi,
+ deleteWallet as deleteWalletApi,
depositWallet as depositWalletApi,
- adjustBalance as adjustBalanceApi,
fetchQuotes,
generateMonthlyExpense as generateMonthlyExpenseApi,
+ generateTelegramCode,
getDailyMinMax,
getDailyQuotes,
getExpenses,
- generateTelegramCode,
getHistoricalMinMax,
getMonthlyPayedTotal,
getMonthlyTotals,
@@ -30,23 +30,25 @@ import {
getPeriodicExpenses,
getQuoteHistory,
getQuotes,
+ getSettings,
getTelegramStatus,
getTotalPending,
getWalletMovements,
getWalletMovementsFiltered,
getWallets,
- setDefaultWallet as setDefaultWalletApi,
importExpenses,
importPeriodicExpenses,
type PayExpenseInput,
payExpense as payExpenseApi,
+ setDefaultWallet as setDefaultWalletApi,
+ type TransferInput,
+ transferWallet as transferWalletApi,
type UpdateExpenseInput,
updateExpense as updateExpenseApi,
updatePeriodicExpense as updatePeriodicExpenseApi,
- createWallet as createWalletApi,
+ updateSetting as updateSettingApi,
updateWallet as updateWalletApi,
- deleteWallet as deleteWalletApi,
- transferWallet as transferWalletApi,
+ type WalletMovementsFilter,
} from "./api";
export const quoteKeys = {
@@ -422,6 +424,31 @@ export function useWalletMovementsFiltered(filter: WalletMovementsFilter) {
});
}
+// Settings
+
+export const settingKeys = {
+ all: ["settings"] as const,
+};
+
+export function useSettings() {
+ return useQuery({
+ queryKey: settingKeys.all,
+ queryFn: getSettings,
+ staleTime: 60_000,
+ });
+}
+
+export function useUpdateSetting() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ key, value }: { key: string; value: string }) =>
+ updateSettingApi(key, value),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: settingKeys.all });
+ },
+ });
+}
+
// Telegram
export function useTelegramStatus() {
diff --git a/apps/frontend/src/routes/_authenticated/settings.tsx b/apps/frontend/src/routes/_authenticated/settings.tsx
index ca471a3..8955522 100644
--- a/apps/frontend/src/routes/_authenticated/settings.tsx
+++ b/apps/frontend/src/routes/_authenticated/settings.tsx
@@ -1,12 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { Check, Copy, RefreshCw } from "lucide-react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth-context";
import {
useGenerateTelegramCode,
+ useSettings,
useTelegramStatus,
+ useUpdateSetting,
} from "@/lib/queries";
export const Route = createFileRoute("/_authenticated/settings")({
@@ -126,11 +128,79 @@ function SettingsPage() {
+
+
);
}
+function SalaryMultiplierSection() {
+ const { data: settings, isLoading } = useSettings();
+ const updateSetting = useUpdateSetting();
+ const current =
+ settings?.find((s) => s.key === "salary_multiplier")?.value ?? "4500";
+ const [localValue, setLocalValue] = useState(current);
+ const [success, setSuccess] = useState(false);
+
+ useEffect(() => {
+ if (settings) {
+ const val = settings.find((s) => s.key === "salary_multiplier")?.value;
+ if (val) setLocalValue(val);
+ }
+ }, [settings]);
+
+ async function handleSave() {
+ setSuccess(false);
+ await updateSetting.mutateAsync({
+ key: "salary_multiplier",
+ value: localValue,
+ });
+ setSuccess(true);
+ setTimeout(() => setSuccess(false), 2000);
+ }
+
+ return (
+
+
+
+ Multiplicador de sueldo
+
+
+ Valor para calcular el sueldo (Sueldo = BELO bid x este valor)
+
+
+
+ {isLoading ? (
+
Cargando...
+ ) : (
+
+
+ setLocalValue(e.target.value)}
+ className="w-32"
+ />
+
+
+ {success && (
+
+ Multiplicador actualizado correctamente
+
+ )}
+
+ )}
+
+ );
+}
+
function TelegramSection() {
const { data: status, isLoading } = useTelegramStatus();
const {