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 {